diff --git a/cashu/cashu.go b/cashu/cashu.go index e656fd1..cdc81af 100644 --- a/cashu/cashu.go +++ b/cashu/cashu.go @@ -12,6 +12,12 @@ import ( "fmt" "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/fxamacker/cbor/v2" +) + +var ( + ErrInvalidTokenV3 = errors.New("invalid V3 token") + ErrInvalidTokenV4 = errors.New("invalid V4 token") ) // Cashu BlindedMessage. See https://github.com/cashubtc/nuts/blob/main/00.md#blindedmessage @@ -86,32 +92,51 @@ func (proofs Proofs) Amount() uint64 { } // Cashu token. See https://github.com/cashubtc/nuts/blob/main/00.md#token-format -type Token struct { - Token []TokenProof `json:"token"` - Unit string `json:"unit"` - Memo string `json:"memo,omitempty"` +type Token interface { + Proofs() Proofs + Mint() string + Amount() uint64 + Serialize() (string, error) +} + +func DecodeToken(tokenstr string) (Token, error) { + token, err := DecodeTokenV4(tokenstr) + if err != nil { + // if err, try decoding as V3 + tokenV3, err := DecodeTokenV3(tokenstr) + if err != nil { + return nil, fmt.Errorf("invalid token: %v", err) + } + return tokenV3, nil + } + return token, nil } -type TokenProof struct { +type TokenV3 struct { + Token []TokenV3Proof `json:"token"` + Unit string `json:"unit"` + Memo string `json:"memo,omitempty"` +} + +type TokenV3Proof struct { Mint string `json:"mint"` Proofs Proofs `json:"proofs"` } -func NewToken(proofs Proofs, mint string, unit string) Token { - tokenProof := TokenProof{Mint: mint, Proofs: proofs} - return Token{Token: []TokenProof{tokenProof}, Unit: unit} +func NewTokenV3(proofs Proofs, mint string, unit string) TokenV3 { + tokenProof := TokenV3Proof{Mint: mint, Proofs: proofs} + return TokenV3{Token: []TokenV3Proof{tokenProof}, Unit: unit} } -func DecodeToken(tokenstr string) (*Token, error) { +func DecodeTokenV3(tokenstr string) (*TokenV3, error) { prefixVersion := tokenstr[:6] base64Token := tokenstr[6:] + if prefixVersion != "cashuA" { - return nil, errors.New("invalid token") + return nil, ErrInvalidTokenV3 } - var tokenBytes []byte - var err error - tokenBytes, err = base64.URLEncoding.DecodeString(base64Token) + tokenBytes, err := base64.URLEncoding.DecodeString(base64Token) if err != nil { tokenBytes, err = base64.RawURLEncoding.DecodeString(base64Token) if err != nil { @@ -119,7 +144,7 @@ func DecodeToken(tokenstr string) (*Token, error) { } } - var token Token + var token TokenV3 err = json.Unmarshal(tokenBytes, &token) if err != nil { return nil, fmt.Errorf("error unmarshaling token: %v", err) @@ -128,20 +153,19 @@ func DecodeToken(tokenstr string) (*Token, error) { return &token, nil } -// ToString serializes the token to a string -func (t *Token) ToString() string { - jsonBytes, err := json.Marshal(t) - if err != nil { - panic(err) +func (t TokenV3) Proofs() Proofs { + proofs := make(Proofs, 0) + for _, tokenProof := range t.Token { + proofs = append(proofs, tokenProof.Proofs...) } + return proofs +} - token := base64.URLEncoding.EncodeToString(jsonBytes) - return "cashuA" + token +func (t TokenV3) Mint() string { + return t.Token[0].Mint } -// TotalAmount returns the total amount -// from the array of Proofs in the token -func (t *Token) TotalAmount() uint64 { +func (t TokenV3) Amount() uint64 { var totalAmount uint64 = 0 for _, tokenProof := range t.Token { for _, proof := range tokenProof.Proofs { @@ -151,6 +175,131 @@ func (t *Token) TotalAmount() uint64 { return totalAmount } +func (t TokenV3) Serialize() (string, error) { + jsonBytes, err := json.Marshal(t) + if err != nil { + return "", err + } + + token := "cashuA" + base64.URLEncoding.EncodeToString(jsonBytes) + return token, nil +} + +type TokenV4 struct { + TokenProofs []TokenV4Proof `json:"t"` + Memo string `json:"d,omitempty"` + MintURL string `json:"m"` + Unit string `json:"u"` +} + +type TokenV4Proof struct { + Id []byte `json:"i"` + Proofs []ProofV4 `json:"p"` +} + +type ProofV4 struct { + Amount uint64 `json:"a"` + Secret string `json:"s"` + C []byte `json:"c"` + Witness string `json:"w,omitempty"` +} + +func NewTokenV4(proofs Proofs, mint string, unit string) (TokenV4, error) { + proofsMap := make(map[string][]ProofV4) + for _, proof := range proofs { + C, err := hex.DecodeString(proof.C) + if err != nil { + return TokenV4{}, fmt.Errorf("invalid C: %v", err) + } + proofV4 := ProofV4{ + Amount: proof.Amount, + Secret: proof.Secret, + C: C, + Witness: proof.Witness, + } + proofsMap[proof.Id] = append(proofsMap[proof.Id], proofV4) + } + + proofsV4 := make([]TokenV4Proof, len(proofsMap)) + i := 0 + for k, v := range proofsMap { + keysetIdBytes, err := hex.DecodeString(k) + if err != nil { + return TokenV4{}, fmt.Errorf("invalid keyset id: %v", err) + } + proofV4 := TokenV4Proof{Id: keysetIdBytes, Proofs: v} + proofsV4[i] = proofV4 + i++ + } + + return TokenV4{MintURL: mint, Unit: unit, TokenProofs: proofsV4}, nil +} + +func DecodeTokenV4(tokenstr string) (*TokenV4, error) { + prefixVersion := tokenstr[:6] + base64Token := tokenstr[6:] + if prefixVersion != "cashuB" { + return nil, ErrInvalidTokenV4 + } + + tokenBytes, err := base64.URLEncoding.DecodeString(base64Token) + if err != nil { + tokenBytes, err = base64.RawURLEncoding.DecodeString(base64Token) + if err != nil { + return nil, fmt.Errorf("error decoding token: %v", err) + } + } + + var tokenV4 TokenV4 + err = cbor.Unmarshal(tokenBytes, &tokenV4) + if err != nil { + return nil, fmt.Errorf("cbor.Unmarshal: %v", err) + } + + return &tokenV4, nil +} + +func (t TokenV4) Proofs() Proofs { + proofs := make(Proofs, 0) + for _, tokenV4Proof := range t.TokenProofs { + keysetId := hex.EncodeToString(tokenV4Proof.Id) + for _, proofV4 := range tokenV4Proof.Proofs { + proof := Proof{ + Amount: proofV4.Amount, + Id: keysetId, + Secret: proofV4.Secret, + C: hex.EncodeToString(proofV4.C), + Witness: proofV4.Witness, + } + proofs = append(proofs, proof) + } + } + return proofs +} + +func (t TokenV4) Mint() string { + return t.MintURL +} + +func (t TokenV4) Amount() uint64 { + var totalAmount uint64 + proofs := t.Proofs() + for _, proof := range proofs { + totalAmount += proof.Amount + } + return totalAmount +} + +func (t TokenV4) Serialize() (string, error) { + cborData, err := cbor.Marshal(t) + if err != nil { + return "", err + } + + token := "cashuB" + base64.RawURLEncoding.EncodeToString(cborData) + return token, nil +} + type CashuErrCode int // Error represents an error to be returned by the mint diff --git a/cashu/cashu_test.go b/cashu/cashu_test.go index 03fb9ea..4ab84e9 100644 --- a/cashu/cashu_test.go +++ b/cashu/cashu_test.go @@ -1,21 +1,209 @@ package cashu import ( + "encoding/hex" "reflect" "testing" ) -func TestDecodeToken(t *testing.T) { +func TestDecodeTokenV4(t *testing.T) { + keysetIdBytes, _ := hex.DecodeString("00ad268c4d1f5826") + Cbytes, _ := hex.DecodeString("038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792") + keysetId2Bytes, _ := hex.DecodeString("00ffd48b8f5ecf80") + C2Bytes, _ := hex.DecodeString("0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf") + C3Bytes, _ := hex.DecodeString("023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d") + C4Bytes, _ := hex.DecodeString("0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63") + + tests := []struct { + tokenString string + expected TokenV4 + }{ + { + tokenString: "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ=", + expected: TokenV4{ + MintURL: "http://localhost:3338", + TokenProofs: []TokenV4Proof{ + { + //Id: []byte("\x00\xad\x26\x8c\x4d\x1f\x58\x26"), + Id: keysetIdBytes, + Proofs: []ProofV4{ + { + Amount: 1, + Secret: "9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e", + //C: []byte("\x03\x86\x18\x54\x3f\xfb\x6b\x86\x95\xdf\x4a\xd4\xba\xbc\xde\x92\xa3\x4a\x96\xbd\xcd\x97\xdc\xee\x0d\x7c\xcf\x98\xd4\x72\x12\x67\x92"), + C: Cbytes, + }, + }, + }, + }, + Unit: "sat", + Memo: "Thank you", + }, + }, + { + tokenString: "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA", + expected: TokenV4{ + MintURL: "http://localhost:3338", + TokenProofs: []TokenV4Proof{ + { + Id: keysetId2Bytes, + Proofs: []ProofV4{ + { + Amount: 1, + Secret: "acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", + C: C2Bytes, + }, + }, + }, + { + Id: keysetIdBytes, + Proofs: []ProofV4{ + { + Amount: 2, + Secret: "1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", + C: C3Bytes, + }, + { + Amount: 1, + Secret: "56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", + C: C4Bytes, + }, + }, + }, + }, + Unit: "sat", + }, + }, + } + + for _, test := range tests { + token, _ := DecodeTokenV4(test.tokenString) + if token.Unit != test.expected.Unit { + t.Errorf("expected '%v' but got '%v' instead", test.expected.Unit, token.Unit) + } + + if token.Memo != test.expected.Memo { + t.Errorf("expected '%v' but got '%v' instead", test.expected.Memo, token.Memo) + } + + if token.Mint() != test.expected.MintURL { + t.Errorf("expected '%v' but got '%v' instead", test.expected.MintURL, token.Mint()) + } + + proofs := token.Proofs() + expectedProofs := test.expected.Proofs() + for i, proof := range proofs { + if proof.Id != expectedProofs[i].Id { + t.Errorf("expected '%v' but got '%v' instead", expectedProofs[i].Id, proof.Id) + } + + if proof.Amount != expectedProofs[i].Amount { + t.Errorf("expected '%v' but got '%v' instead", test.expected.TokenProofs[0].Proofs[i].Amount, proof.Amount) + } + + if proof.Secret != expectedProofs[i].Secret { + t.Errorf("expected '%v' but got '%v' instead", test.expected.TokenProofs[0].Proofs[i].Secret, proof.Secret) + } + + if proof.C != expectedProofs[i].C { + t.Errorf("expected '%v' but got '%v' instead", expectedProofs[i].C, proof.C) + } + } + } +} + +func TestSerializeTokenV4(t *testing.T) { + keysetBytes, _ := hex.DecodeString("00ad268c4d1f5826") + C, _ := hex.DecodeString("038618543ffb6b8695df4ad4babcde92a34a96bdcd97dcee0d7ccf98d472126792") + + keysetId2Bytes, _ := hex.DecodeString("00ffd48b8f5ecf80") + C2Bytes, _ := hex.DecodeString("0244538319de485d55bed3b29a642bee5879375ab9e7a620e11e48ba482421f3cf") + C3Bytes, _ := hex.DecodeString("023456aa110d84b4ac747aebd82c3b005aca50bf457ebd5737a4414fac3ae7d94d") + C4Bytes, _ := hex.DecodeString("0273129c5719e599379a974a626363c333c56cafc0e6d01abe46d5808280789c63") + + tests := []struct { + token TokenV4 + expected string + }{ + { + token: TokenV4{ + TokenProofs: []TokenV4Proof{ + { + Id: keysetBytes, + Proofs: []ProofV4{ + { + Amount: 1, + Secret: "9a6dbb847bd232ba76db0df197216b29d3b8cc14553cd27827fc1cc942fedb4e", + C: C, + }, + }, + }, + }, + Memo: "Thank you", + MintURL: "http://localhost:3338", + Unit: "sat", + }, + expected: "cashuBpGF0gaJhaUgArSaMTR9YJmFwgaNhYQFhc3hAOWE2ZGJiODQ3YmQyMzJiYTc2ZGIwZGYxOTcyMTZiMjlkM2I4Y2MxNDU1M2NkMjc4MjdmYzFjYzk0MmZlZGI0ZWFjWCEDhhhUP_trhpXfStS6vN6So0qWvc2X3O4NfM-Y1HISZ5JhZGlUaGFuayB5b3VhbXVodHRwOi8vbG9jYWxob3N0OjMzMzhhdWNzYXQ", + }, + { + token: TokenV4{ + MintURL: "http://localhost:3338", + Unit: "sat", + TokenProofs: []TokenV4Proof{ + { + Id: keysetId2Bytes, + Proofs: []ProofV4{ + { + Amount: 1, + Secret: "acc12435e7b8484c3cf1850149218af90f716a52bf4a5ed347e48ecc13f77388", + C: C2Bytes, + }, + }, + }, + { + Id: keysetBytes, + Proofs: []ProofV4{ + { + Amount: 2, + Secret: "1323d3d4707a58ad2e23ada4e9f1f49f5a5b4ac7b708eb0d61f738f48307e8ee", + C: C3Bytes, + }, + { + Amount: 1, + Secret: "56bcbcbb7cc6406b3fa5d57d2174f4eff8b4402b176926d3a57d3c3dcbb59d57", + C: C4Bytes, + }, + }, + }, + }, + }, + expected: "cashuBo2F0gqJhaUgA_9SLj17PgGFwgaNhYQFhc3hAYWNjMTI0MzVlN2I4NDg0YzNjZjE4NTAxNDkyMThhZjkwZjcxNmE1MmJmNGE1ZWQzNDdlNDhlY2MxM2Y3NzM4OGFjWCECRFODGd5IXVW-07KaZCvuWHk3WrnnpiDhHki6SCQh88-iYWlIAK0mjE0fWCZhcIKjYWECYXN4QDEzMjNkM2Q0NzA3YTU4YWQyZTIzYWRhNGU5ZjFmNDlmNWE1YjRhYzdiNzA4ZWIwZDYxZjczOGY0ODMwN2U4ZWVhY1ghAjRWqhENhLSsdHrr2Cw7AFrKUL9Ffr1XN6RBT6w659lNo2FhAWFzeEA1NmJjYmNiYjdjYzY0MDZiM2ZhNWQ1N2QyMTc0ZjRlZmY4YjQ0MDJiMTc2OTI2ZDNhNTdkM2MzZGNiYjU5ZDU3YWNYIQJzEpxXGeWZN5qXSmJjY8MzxWyvwObQGr5G1YCCgHicY2FtdWh0dHA6Ly9sb2NhbGhvc3Q6MzMzOGF1Y3NhdA", + }, + } + + for _, test := range tests { + tokenString, err := test.token.Serialize() + if err != nil { + t.Fatal(err) + } + + if tokenString != test.expected { + t.Errorf("expected '%v'\n\n but got '%v' instead", test.expected, tokenString) + } + } +} + +func TestDecodeTokenV3(t *testing.T) { tests := []struct { tokenString string tokenWithPadding string - expected Token + expected TokenV3 }{ { tokenString: "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ", tokenWithPadding: "cashuAeyJ0b2tlbiI6W3sibWludCI6Imh0dHBzOi8vODMzMy5zcGFjZTozMzM4IiwicHJvb2ZzIjpbeyJhbW91bnQiOjIsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6IjQwNzkxNWJjMjEyYmU2MWE3N2UzZTZkMmFlYjRjNzI3OTgwYmRhNTFjZDA2YTZhZmMyOWUyODYxNzY4YTc4MzciLCJDIjoiMDJiYzkwOTc5OTdkODFhZmIyY2M3MzQ2YjVlNDM0NWE5MzQ2YmQyYTUwNmViNzk1ODU5OGE3MmYwY2Y4NTE2M2VhIn0seyJhbW91bnQiOjgsImlkIjoiMDA5YTFmMjkzMjUzZTQxZSIsInNlY3JldCI6ImZlMTUxMDkzMTRlNjFkNzc1NmIwZjhlZTBmMjNhNjI0YWNhYTNmNGUwNDJmNjE0MzNjNzI4YzcwNTdiOTMxYmUiLCJDIjoiMDI5ZThlNTA1MGI4OTBhN2Q2YzA5NjhkYjE2YmMxZDVkNWZhMDQwZWExZGUyODRmNmVjNjlkNjEyOTlmNjcxMDU5In1dfV0sInVuaXQiOiJzYXQiLCJtZW1vIjoiVGhhbmsgeW91IHZlcnkgbXVjaC4ifQ==", - expected: Token{ - Token: []TokenProof{ + expected: TokenV3{ + Token: []TokenV3Proof{ { Mint: "https://8333.space:3338", Proofs: Proofs{ @@ -41,12 +229,12 @@ func TestDecodeToken(t *testing.T) { } for _, test := range tests { - token, _ := DecodeToken(test.tokenString) + token, _ := DecodeTokenV3(test.tokenString) if token.Unit != test.expected.Unit { t.Errorf("expected '%v' but got '%v' instead", test.expected.Unit, token.Unit) } - tokenPadding, _ := DecodeToken(test.tokenWithPadding) + tokenPadding, _ := DecodeTokenV3(test.tokenWithPadding) if !reflect.DeepEqual(token, tokenPadding) { t.Error("decoded tokens do not match") } @@ -79,14 +267,14 @@ func TestDecodeToken(t *testing.T) { } } -func TestTokenToString(t *testing.T) { +func TestSerializeTokenV3(t *testing.T) { tests := []struct { - token Token + token TokenV3 expected string }{ { - token: Token{ - Token: []TokenProof{ + token: TokenV3{ + Token: []TokenV3Proof{ { Mint: "https://8333.space:3338", Proofs: Proofs{ @@ -114,7 +302,10 @@ func TestTokenToString(t *testing.T) { } for _, test := range tests { - tokenString := test.token.ToString() + tokenString, err := test.token.Serialize() + if err != nil { + t.Fatal(err) + } if tokenString != test.expected { t.Errorf("expected '%v'\n\n but got '%v' instead", test.expected, tokenString) diff --git a/cmd/nutw/nutw.go b/cmd/nutw/nutw.go index d998c93..bda7b15 100644 --- a/cmd/nutw/nutw.go +++ b/cmd/nutw/nutw.go @@ -169,7 +169,7 @@ func receive(ctx *cli.Context) error { swap := true trustedMints := nutw.TrustedMints() - mintURL := token.Token[0].Mint + mintURL := token.Mint() isTrusted := slices.Contains(trustedMints, mintURL) if !isTrusted { @@ -193,7 +193,7 @@ func receive(ctx *cli.Context) error { swap = false } - receivedAmount, err := nutw.Receive(*token, swap) + receivedAmount, err := nutw.Receive(token, swap) if err != nil { printErr(err) } @@ -275,8 +275,11 @@ func mintTokens(paymentRequest string) error { return nil } -const lockFlag = "lock" -const noFeesFlag = "no-fees" +const ( + lockFlag = "lock" + noFeesFlag = "no-fees" + legacyFlag = "legacy" +) var sendCmd = &cli.Command{ Name: "send", @@ -293,6 +296,11 @@ var sendCmd = &cli.Command{ Usage: "do not include fees for receiver in the token generated", DisableDefaultText: true, }, + &cli.BoolFlag{ + Name: legacyFlag, + Usage: "generate token in legacy (V3) format", + DisableDefaultText: true, + }, }, Action: send, } @@ -315,7 +323,7 @@ func send(ctx *cli.Context) error { includeFees = false } - var token *cashu.Token + var proofsToSend cashu.Proofs // if lock flag is set, get ecash locked to the pubkey if ctx.IsSet(lockFlag) { lockpubkey := ctx.String(lockFlag) @@ -328,18 +336,33 @@ func send(ctx *cli.Context) error { printErr(err) } - token, err = nutw.SendToPubkey(sendAmount, selectedMint, pubkey, includeFees) + proofsToSend, err = nutw.SendToPubkey(sendAmount, selectedMint, pubkey, includeFees) if err != nil { printErr(err) } } else { - token, err = nutw.Send(sendAmount, selectedMint, includeFees) + proofsToSend, err = nutw.Send(sendAmount, selectedMint, includeFees) if err != nil { printErr(err) } } - fmt.Printf("%v\n", token.ToString()) + var token cashu.Token + if ctx.Bool(legacyFlag) { + token = cashu.NewTokenV3(proofsToSend, selectedMint, "sat") + } else { + token, err = cashu.NewTokenV4(proofsToSend, selectedMint, "sat") + if err != nil { + printErr(fmt.Errorf("could not serialize token: %v", err)) + } + } + + tokenString, err := token.Serialize() + if err != nil { + printErr(fmt.Errorf("could not serialize token: %v", err)) + } + fmt.Printf("%v\n", tokenString) + return nil } @@ -515,8 +538,7 @@ func decode(ctx *cli.Context) error { if err != nil { printErr(err) } - - fmt.Println(string(jsonToken)) + fmt.Printf("token: %s\n", jsonToken) return nil } diff --git a/go.mod b/go.mod index 10d4ed4..6b2007e 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/btcsuite/btcd/btcutil v1.1.5 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 github.com/elnosh/btc-docker-test v0.0.0-20240730150514-6d94d76b8881 + github.com/fxamacker/cbor/v2 v2.7.0 github.com/golang-migrate/migrate/v4 v4.17.1 github.com/gorilla/mux v1.8.0 github.com/joho/godotenv v1.5.1 @@ -153,6 +154,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect diff --git a/go.sum b/go.sum index d76a068..c0ae06f 100644 --- a/go.sum +++ b/go.sum @@ -202,6 +202,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= @@ -532,8 +534,7 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= -github.com/opencontainers/runc v1.1.12 h1:BOIssBaW1La0/qbNZHXOOa71dZfZEQOzW7dqQf3phss= -github.com/opencontainers/runc v1.1.12/go.mod h1:S+lQwSfncpBha7XTy/5lBwWgm5+y5Ma/O44Ekby9FK8= +github.com/opencontainers/runc v1.1.14 h1:rgSuzbmgz5DUJjeSnw337TxDbRuqjs6iqQck/2weR6w= github.com/opencontainers/runc v1.1.14/go.mod h1:E4C2z+7BxR7GHXp0hAY53mek+x49X1LjPNeMTfRGvOA= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= @@ -635,6 +636,8 @@ github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2n github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs= github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/wallet/wallet.go b/wallet/wallet.go index 94c7459..f71877e 100644 --- a/wallet/wallet.go +++ b/wallet/wallet.go @@ -426,7 +426,7 @@ func (w *Wallet) MintTokens(quoteId string) (cashu.Proofs, error) { } // Send will return a cashu token with proofs for the given amount -func (w *Wallet) Send(amount uint64, mintURL string, includeFees bool) (*cashu.Token, error) { +func (w *Wallet) Send(amount uint64, mintURL string, includeFees bool) (cashu.Proofs, error) { selectedMint, ok := w.mints[mintURL] if !ok { return nil, ErrMintNotExist @@ -437,8 +437,7 @@ func (w *Wallet) Send(amount uint64, mintURL string, includeFees bool) (*cashu.T return nil, err } - token := cashu.NewToken(proofsToSend, mintURL, "sat") - return &token, nil + return proofsToSend, nil } // SendToPubkey returns a cashu token with proofs that are locked to @@ -448,7 +447,7 @@ func (w *Wallet) SendToPubkey( mintURL string, pubkey *btcec.PublicKey, includeFees bool, -) (*cashu.Token, error) { +) (cashu.Proofs, error) { selectedMint, ok := w.mints[mintURL] if !ok { return nil, ErrMintNotExist @@ -469,13 +468,15 @@ func (w *Wallet) SendToPubkey( return nil, err } - token := cashu.NewToken(lockedProofs, mintURL, "sat") - return &token, nil + return lockedProofs, nil } // Receives Cashu token. If swap is true, it will swap the funds to the configured default mint. // If false, it will add the proofs from the mint and add that mint to the list of trusted mints. func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) { + proofsToSwap := token.Proofs() + tokenMint := token.Mint() + if swapToTrusted { trustedMintProofs, err := w.swapToTrusted(token) if err != nil { @@ -483,26 +484,19 @@ func (w *Wallet) Receive(token cashu.Token, swapToTrusted bool) (uint64, error) } return trustedMintProofs.Amount(), nil } else { - proofsToSwap := make(cashu.Proofs, 0) - for _, tokenProof := range token.Token { - proofsToSwap = append(proofsToSwap, tokenProof.Proofs...) - } - - tokenMintURL := token.Token[0].Mint // only add mint if not previously trusted - _, ok := w.mints[tokenMintURL] + _, ok := w.mints[tokenMint] if !ok { - _, err := w.addMint(tokenMintURL) + _, err := w.addMint(tokenMint) if err != nil { return 0, err } } - proofs, err := w.swap(proofsToSwap, tokenMintURL) + proofs, err := w.swap(proofsToSwap, tokenMint) if err != nil { return 0, err } - w.saveProofs(proofs) return proofs.Amount(), nil @@ -597,14 +591,10 @@ func (w *Wallet) swap(proofsToSwap cashu.Proofs, mintURL string) (cashu.Proofs, // to the wallet's configured default mint func (w *Wallet) swapToTrusted(token cashu.Token) (cashu.Proofs, error) { invoicePct := 0.99 - tokenAmount := token.TotalAmount() - tokenMintURL := token.Token[0].Mint + tokenAmount := token.Amount() amount := float64(tokenAmount) * invoicePct - - proofsToSwap := make(cashu.Proofs, 0) - for _, tokenProof := range token.Token { - proofsToSwap = append(proofsToSwap, tokenProof.Proofs...) - } + tokenMintURL := token.Mint() + proofsToSwap := token.Proofs() var mintResponse *nut04.PostMintQuoteBolt11Response var meltQuoteResponse *nut05.PostMeltQuoteBolt11Response diff --git a/wallet/wallet_integration_test.go b/wallet/wallet_integration_test.go index 097b1a6..9fddba6 100644 --- a/wallet/wallet_integration_test.go +++ b/wallet/wallet_integration_test.go @@ -13,6 +13,7 @@ import ( "testing" btcdocker "github.com/elnosh/btc-docker-test" + "github.com/elnosh/gonuts/cashu" "github.com/elnosh/gonuts/testutils" "github.com/elnosh/gonuts/wallet" "github.com/lightningnetwork/lnd/lnrpc" @@ -169,12 +170,12 @@ func TestSend(t *testing.T) { } var sendAmount uint64 = 4200 - token, err := testWallet.Send(sendAmount, mintURL, true) + proofsToSend, err := testWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("got unexpected error: %v", err) } - if token.TotalAmount() != sendAmount { - t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount, token.TotalAmount()) + if proofsToSend.Amount() != sendAmount { + t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount, proofsToSend.Amount()) } // test with invalid mint @@ -206,26 +207,26 @@ func TestSend(t *testing.T) { } sendAmount = 2000 - token, err = feesWallet.Send(sendAmount, mintWithFeesURL, true) + proofsToSend, err = feesWallet.Send(sendAmount, mintWithFeesURL, true) if err != nil { t.Fatalf("got unexpected error: %v", err) } - fees, err := testutils.Fees(token.Token[0].Proofs, mintWithFeesURL) + fees, err := testutils.Fees(proofsToSend, mintWithFeesURL) if err != nil { t.Fatalf("got unexpected error: %v", err) } - if token.TotalAmount() != sendAmount+uint64(fees) { - t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), token.TotalAmount()) + if proofsToSend.Amount() != sendAmount+uint64(fees) { + t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), proofsToSend.Amount()) } // send without fees to receive - token, err = feesWallet.Send(sendAmount, mintWithFeesURL, false) + proofsToSend, err = feesWallet.Send(sendAmount, mintWithFeesURL, false) if err != nil { t.Fatalf("got unexpected error: %v", err) } - if token.TotalAmount() != sendAmount { - t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), token.TotalAmount()) + if proofsToSend.Amount() != sendAmount { + t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), proofsToSend.Amount()) } } @@ -272,13 +273,14 @@ func TestReceive(t *testing.T) { t.Fatalf("error funding wallet: %v", err) } - token, err := testWallet2.Send(1500, mint2URL, true) + proofsToSend, err := testWallet2.Send(1500, mint2URL, true) if err != nil { t.Fatalf("got unexpected error in send: %v", err) } + token, _ := cashu.NewTokenV4(proofsToSend, mint2URL, testutils.SAT_UNIT) // test receive swap == true - _, err = testWallet.Receive(*token, true) + _, err = testWallet.Receive(token, true) if err != nil { t.Fatalf("got unexpected error in receive: %v", err) } @@ -292,13 +294,14 @@ func TestReceive(t *testing.T) { t.Fatalf("expected '%v' in list of trusted of trusted mints", defaultMint) } - token2, err := testWallet2.Send(1500, mint2URL, true) + proofsToSend, err = testWallet2.Send(1500, mint2URL, true) if err != nil { t.Fatalf("got unexpected error in send: %v", err) } + token, _ = cashu.NewTokenV4(proofsToSend, mint2URL, testutils.SAT_UNIT) // test receive swap == false - _, err = testWallet.Receive(*token2, false) + _, err = testWallet.Receive(token, false) if err != nil { t.Fatalf("got unexpected error in receive: %v", err) } @@ -340,23 +343,24 @@ func TestReceiveFees(t *testing.T) { }() var sendAmount uint64 = 2000 - token, err := testWallet.Send(sendAmount, mintURL, true) + proofsToSend, err := testWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("got unexpected error in send: %v", err) } + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) - amountReceived, err := testWallet2.Receive(*token, false) + amountReceived, err := testWallet2.Receive(token, false) if err != nil { t.Fatalf("got unexpected error in receive: %v", err) } - fees, err := testutils.Fees(token.Token[0].Proofs, mintURL) + fees, err := testutils.Fees(proofsToSend, mintURL) if err != nil { t.Fatalf("got unexpected error: %v", err) } - if amountReceived != token.TotalAmount()-uint64(fees) { - t.Fatalf("expected received amount of '%v' but got '%v' instead", token.TotalAmount()-uint64(fees), amountReceived) + if amountReceived != proofsToSend.Amount()-uint64(fees) { + t.Fatalf("expected received amount of '%v' but got '%v' instead", proofsToSend.Amount()-uint64(fees), amountReceived) } } @@ -538,14 +542,15 @@ func TestWalletBalanceFees(t *testing.T) { sendAmounts := []uint64{1200, 2000, 5000} for _, sendAmount := range sendAmounts { - token, err := balanceTestWallet.Send(sendAmount, mintURL, true) + proofsToSend, err := balanceTestWallet.Send(sendAmount, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) // test balance in receiving wallet balanceBeforeReceive := balanceTestWallet2.GetBalance() - _, err = balanceTestWallet2.Receive(*token, false) + _, err = balanceTestWallet2.Receive(token, false) if err != nil { t.Fatalf("got unexpected error: %v", err) } @@ -557,19 +562,20 @@ func TestWalletBalanceFees(t *testing.T) { // test without including fees in send for _, sendAmount := range sendAmounts { - token, err := balanceTestWallet.Send(sendAmount, mintURL, false) + proofsToSend, err := balanceTestWallet.Send(sendAmount, mintURL, false) if err != nil { t.Fatalf("unexpected error in send: %v", err) } + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) - fees, err := testutils.Fees(token.Token[0].Proofs, mintURL) + fees, err := testutils.Fees(proofsToSend, mintURL) if err != nil { t.Fatalf("got unexpected error: %v", err) } // test balance in receiving wallet balanceBeforeReceive := balanceTestWallet2.GetBalance() - _, err = balanceTestWallet2.Receive(*token, false) + _, err = balanceTestWallet2.Receive(token, false) if err != nil { t.Fatalf("got unexpected error: %v", err) } @@ -658,19 +664,20 @@ func testP2PK( } receiverPubkey := testWallet2.GetReceivePubkey() - lockedEcash, err := testWallet.SendToPubkey(500, testWallet.CurrentMint(), receiverPubkey, true) + lockedProofs, err := testWallet.SendToPubkey(500, testWallet.CurrentMint(), receiverPubkey, true) if err != nil { t.Fatalf("unexpected error generating locked ecash: %v", err) } + lockedEcash, _ := cashu.NewTokenV4(lockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT) // try receiving invalid - _, err = testWallet.Receive(*lockedEcash, true) + _, err = testWallet.Receive(lockedEcash, true) if err == nil { t.Fatal("expected error trying to redeem locked ecash") } // this should unlock ecash and swap to trusted mint - amountReceived, err := testWallet2.Receive(*lockedEcash, true) + amountReceived, err := testWallet2.Receive(lockedEcash, true) if err != nil { t.Fatalf("unexpected error receiving locked ecash: %v", err) } @@ -685,13 +692,14 @@ func testP2PK( t.Fatalf("expected balance of '%v' but got '%v' instead", amountReceived, balance) } - lockedEcash, err = testWallet.SendToPubkey(500, testWallet.CurrentMint(), receiverPubkey, true) + lockedProofs, err = testWallet.SendToPubkey(500, testWallet.CurrentMint(), receiverPubkey, true) if err != nil { t.Fatalf("unexpected error generating locked ecash: %v", err) } + lockedEcash, _ = cashu.NewTokenV4(lockedProofs, testWallet.CurrentMint(), testutils.SAT_UNIT) // unlock ecash and trust mint - amountReceived, err = testWallet2.Receive(*lockedEcash, false) + amountReceived, err = testWallet2.Receive(lockedEcash, false) if err != nil { t.Fatalf("unexpected error receiving locked ecash: %v", err) } @@ -734,24 +742,25 @@ func TestNutshell(t *testing.T) { } var sendAmount uint64 = 2000 - token, err := testWallet.Send(sendAmount, nutshellURL, true) + proofsToSend, err := testWallet.Send(sendAmount, nutshellURL, true) if err != nil { t.Fatalf("got unexpected error: %v", err) } + token, _ := cashu.NewTokenV4(proofsToSend, nutshellURL, testutils.SAT_UNIT) - fees, _ := testutils.Fees(token.Token[0].Proofs, nutshellURL) - if token.TotalAmount() != sendAmount+uint64(fees) { - t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), token.TotalAmount()) + fees, _ := testutils.Fees(proofsToSend, nutshellURL) + if proofsToSend.Amount() != sendAmount+uint64(fees) { + t.Fatalf("expected token amount of '%v' but got '%v' instead", sendAmount+uint64(fees), proofsToSend.Amount()) } - amountReceived, err := testWallet.Receive(*token, false) + amountReceived, err := testWallet.Receive(token, false) if err != nil { t.Fatalf("unexpected error receiving: %v", err) } - fees, _ = testutils.Fees(token.Token[0].Proofs, nutshellURL) - if amountReceived != token.TotalAmount()-uint64(fees) { - t.Fatalf("expected received amount of '%v' but got '%v' instead", token.TotalAmount()-uint64(fees), amountReceived) + fees, _ = testutils.Fees(proofsToSend, nutshellURL) + if amountReceived != proofsToSend.Amount()-uint64(fees) { + t.Fatalf("expected received amount of '%v' but got '%v' instead", proofsToSend.Amount()-uint64(fees), amountReceived) } } @@ -828,23 +837,25 @@ func TestWalletRestore(t *testing.T) { } var sendAmount1 uint64 = 5000 - token, err := testWallet.Send(sendAmount1, mintURL, true) + proofsToSend, err := testWallet.Send(sendAmount1, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } + token, _ := cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) - _, err = testWallet2.Receive(*token, false) + _, err = testWallet2.Receive(token, false) if err != nil { t.Fatalf("got unexpected error in receive: %v", err) } var sendAmount2 uint64 = 1000 - token, err = testWallet.Send(sendAmount2, mintURL, true) + proofsToSend, err = testWallet.Send(sendAmount2, mintURL, true) if err != nil { t.Fatalf("unexpected error in send: %v", err) } + token, _ = cashu.NewTokenV4(proofsToSend, mintURL, testutils.SAT_UNIT) - _, err = testWallet2.Receive(*token, false) + _, err = testWallet2.Receive(token, false) if err != nil { t.Fatalf("got unexpected error in receive: %v", err) }