diff --git a/.gitignore b/.gitignore index 9c8a4480e..10f9465c1 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ handover*.md ulw-*.md # Captive portal build output (built from source via packaging/portal-build.sh) packaging/files/tollgate-captive-portal-site/assets/ +nuts/ diff --git a/specquotes.toml b/specquotes.toml new file mode 100644 index 000000000..a877b83cf --- /dev/null +++ b/specquotes.toml @@ -0,0 +1,21 @@ +# greatspectations spec-quote drift config for tollgate-module-basic-go +# +# Tool: https://github.com/rustyrussell/greatspectations +# Spec: https://github.com/cashubtc/nuts (Cashu NUT specifications) +# +# Comment marker syntax in Go source: +# // NUT #00: +# // NUT #05: +# +# Setup (one-time, local): +# git clone --depth=1 https://github.com/cashubtc/nuts.git nuts +# pip install git+https://github.com/rustyrussell/greatspectations.git +# +# Run: +# spectate check --config specquotes.toml --comment-start '//' --comment-continue '//' 'src/**/*.go' +# spectate coverage --config specquotes.toml + +[sources.NUT] +format = "markdown" +dir = "nuts" +pattern = "{id:02d}.md" diff --git a/src/main.go b/src/main.go index 15cf18bf1..feda56af1 100644 --- a/src/main.go +++ b/src/main.go @@ -407,6 +407,8 @@ func handleDetails(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, merchantProvider.inner.GetMerchant().GetAdvertisement()) } +// NUT #00: `cashu` is the Cashu token prefix. `[version]` is a single `base64_urlsafe` character to denote the token format version. + // handleRootPost handles POST requests to the root endpoint func extractCashuToken(body []byte) (token string, event *nostr.Event) { var ev nostr.Event @@ -422,6 +424,7 @@ func extractCashuToken(body []byte) (token string, event *nostr.Event) { return strings.TrimSpace(string(body)), nil } +// NUT #00: Serialized tokens have a Cashu token prefix, a versioning flag, and the token. func HandleRootPost(w http.ResponseWriter, r *http.Request) { // Log the request details mainLogger.WithFields(logrus.Fields{ diff --git a/src/merchant/merchant.go b/src/merchant/merchant.go index 7d16aa26a..1f98e6f82 100644 --- a/src/merchant/merchant.go +++ b/src/merchant/merchant.go @@ -442,6 +442,7 @@ type PurchaseSessionResult struct { } // PurchaseSession processes a payment with cashu token and MAC address, returns either a session event or a notice event +// NUT-00 verification quote lives above TollWallet.Receive (single source; duplicate quotes flag in speccheck). func (m *Merchant) PurchaseSession(cashuToken string, macAddress string) (*nostr.Event, error) { // Validate MAC address if !utils.ValidateMACAddress(macAddress) { diff --git a/src/tollwallet/tollwallet.go b/src/tollwallet/tollwallet.go index 769b3ca24..1c80a59c5 100644 --- a/src/tollwallet/tollwallet.go +++ b/src/tollwallet/tollwallet.go @@ -116,6 +116,7 @@ func (w *TollWallet) Shutdown() error { return nil } +// NUT #00: `Carol` can send `(x, C)` to `Bob` who then checks that `k*hash_to_curve(x) == C` (**verification**), and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. func (w *TollWallet) Receive(token cashu.Token) (uint64, error) { mint := token.Mint() @@ -151,6 +152,7 @@ func (w *TollWallet) Receive(token cashu.Token) (uint64, error) { return amountAfterSwap, nil } +// NUT #03: The swap operation is the most important component of the Cashu system. A swap operation consists of multiple inputs (`Proofs`) and outputs (`BlindedMessages`). Mints verify and invalidate the inputs and issue new promises (`BlindSignatures`). These are then used by the wallet to generate new Proofs (see [NUT-00][00]). func (w *TollWallet) Send(amount uint64, mintUrl string, includeFees bool) (cashu.Token, error) { log.Printf("TollWallet.Send: attempting to send %d sats from mint %s (includeFees=%t)", amount, mintUrl, includeFees) @@ -269,11 +271,13 @@ func (w *TollWallet) SendWithOverpayment(amount uint64, mintUrl string, maxOverp return tokenString, nil } +// NUT #04: To request a mint quote, the wallet of `Alice` makes a `POST /v1/mint/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). `method` **MUST** match `[a-z0-9_-]+`. func (w *TollWallet) RequestMintQuote(amount uint64, mintURL string) (*nut04.PostMintQuoteBolt11Response, error) { w.ensureMintRegistered(mintURL) return w.wallet.RequestMint(amount, mintURL) } +// NUT #04: To check the current accounting data of a mint quote, the wallet makes a `GET /v1/mint/quote/{method}/{quote_id}`. func (w *TollWallet) GetMintQuoteState(quoteID string) (*nut04.PostMintQuoteBolt11Response, error) { if w.wallet == nil { return nil, ErrWalletNotInitialized @@ -281,6 +285,7 @@ func (w *TollWallet) GetMintQuoteState(quoteID string) (*nut04.PostMintQuoteBolt return w.wallet.MintQuoteState(quoteID) } +// NUT #04: After requesting a mint quote and paying the request, the wallet proceeds with minting new tokens by calling the `POST /v1/mint/{method}` endpoint. func (w *TollWallet) MintQuoteTokens(quoteID string) (uint64, error) { return w.wallet.MintTokens(quoteID) } @@ -367,6 +372,8 @@ func (w *TollWallet) GetAllMintBalances() map[string]uint64 { return w.wallet.GetBalanceByMints() } +// NUT #05: To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). `method` **MUST** match `[a-z0-9_-]+`. + // MeltToLightning melts a token to a lightning invoice using LNURL // It attempts to melt for the target amount, reducing by 5% each time if fees are too high func (w *TollWallet) MeltToLightning(mintUrl string, targetAmount uint64, maxCost uint64, lnurl string) error {