@@ -18,6 +18,10 @@ import (
18
18
// BaseURL is the API URL for the Wallet of Satoshi API.
19
19
const BaseURL = "https://www.livingroomofsatoshi.com"
20
20
21
+ // ErrOutsideSendableRange is returned when sending to a lightning address, but the amount
22
+ // the caller asks to send is outside the range accepted by the receiver.
23
+ var ErrOutsideSendableRange = errors .New ("amount to send to LN address is outside the recipient's accepted range" )
24
+
21
25
type errorResponse struct {
22
26
Message string
23
27
}
@@ -47,6 +51,14 @@ func checkHTTPResponse(resp *http.Response) error {
47
51
return err
48
52
}
49
53
54
+ func fromMillisat (sat uint64 ) float64 {
55
+ return float64 (sat ) / 100_000_000.0 / 1_000.0
56
+ }
57
+
58
+ func toMillisat (amount float64 ) uint64 {
59
+ return uint64 (amount * 100_000_000 * 1_000 )
60
+ }
61
+
50
62
// Credentials represents a full set of credentials for a WoS wallet.
51
63
type Credentials struct {
52
64
// APISecret is a base58 secret needed for write-access to a wallet.
@@ -102,7 +114,7 @@ type Wallet struct {
102
114
signer Signer
103
115
httpClient * http.Client
104
116
onChainAddress string
105
- lightningAddress string
117
+ lightningAddress LightningAddress
106
118
}
107
119
108
120
// OpenWallet opens an existing wallet using a separate [Reader] and [Signer].
@@ -115,12 +127,17 @@ func OpenWallet(ctx context.Context, reader *Reader, signer Signer) (*Wallet, er
115
127
return nil , fmt .Errorf ("OpenWallet: %w" , err )
116
128
}
117
129
130
+ lnAddress , err := ParseLightningAddress (addresses .Lightning )
131
+ if err != nil {
132
+ return nil , fmt .Errorf ("OpenWallet: %w" , err )
133
+ }
134
+
118
135
wallet := & Wallet {
119
136
reader : reader ,
120
137
signer : signer ,
121
138
httpClient : reader .httpClient ,
122
139
onChainAddress : addresses .OnChain ,
123
- lightningAddress : addresses . Lightning ,
140
+ lightningAddress : lnAddress ,
124
141
}
125
142
126
143
return wallet , nil
@@ -165,6 +182,11 @@ func CreateWallet(ctx context.Context, httpClient *http.Client) (*Wallet, *Crede
165
182
return nil , nil , fmt .Errorf ("error decoding CreateWallet response: %w" , err )
166
183
}
167
184
185
+ lnAddress , err := ParseLightningAddress (respStruct .LightningAddress )
186
+ if err != nil {
187
+ return nil , nil , fmt .Errorf ("CreateWallet: %w" , err )
188
+ }
189
+
168
190
creds := & Credentials {
169
191
APISecret : respStruct .APISecret ,
170
192
APIToken : respStruct .APIToken ,
@@ -175,14 +197,14 @@ func CreateWallet(ctx context.Context, httpClient *http.Client) (*Wallet, *Crede
175
197
signer : creds .SimpleSigner (),
176
198
httpClient : httpClient ,
177
199
onChainAddress : respStruct .OnChainAddress ,
178
- lightningAddress : respStruct . LightningAddress ,
200
+ lightningAddress : lnAddress ,
179
201
}
180
202
181
203
return wallet , creds , nil
182
204
}
183
205
184
206
// LightningAddress returns the wallet's static Lightning Address.
185
- func (wallet * Wallet ) LightningAddress () string {
207
+ func (wallet * Wallet ) LightningAddress () LightningAddress {
186
208
return wallet .lightningAddress
187
209
}
188
210
@@ -389,6 +411,64 @@ func (wallet *Wallet) PayInvoice(ctx context.Context, invoice, description strin
389
411
})
390
412
}
391
413
414
+ // PayLightningAddress executes a payment of the given BTC amount to a
415
+ // given lightning address. The description is stored in the WoS payment history.
416
+ //
417
+ // Returns ErrOutsideSendableRange if the amount to be sent is outside the receiver's
418
+ // acceptable min/max sendable range.
419
+ //
420
+ // Under the hood, this uses the WoS API to proxy your request to [LightningAddress.Domain],
421
+ // so that the recipient does not see your IP address.
422
+ func (wallet * Wallet ) PayLightningAddress (
423
+ ctx context.Context ,
424
+ lnAddress LightningAddress ,
425
+ description string ,
426
+ amount float64 ,
427
+ ) (* Payment , error ) {
428
+ respData , err := wallet .PostRequest (ctx , "/api/v1/wallet/lnurl" , map [string ]any {
429
+ "address" : lnAddress .LNURL (),
430
+ })
431
+ if err != nil {
432
+ return nil , fmt .Errorf ("PayLightningAddress: %w" , err )
433
+ }
434
+
435
+ var lnPayResponseBody struct {
436
+ Callback string `json:"callback"`
437
+ MaxSendable uint64 `json:"maxSendable"`
438
+ MinSendable uint64 `json:"minSendable"`
439
+ }
440
+
441
+ if err := json .Unmarshal (respData , & lnPayResponseBody ); err != nil {
442
+ return nil , fmt .Errorf ("PayLightningAddress: invalid response JSON: %w" , err )
443
+ }
444
+
445
+ if maxSendable := fromMillisat (lnPayResponseBody .MaxSendable ); amount > maxSendable {
446
+ return nil , fmt .Errorf (
447
+ "PayLightningAddress: %w: exceeds maxSendable (%f BTC)" ,
448
+ ErrOutsideSendableRange , maxSendable ,
449
+ )
450
+ } else if minSendable := fromMillisat (lnPayResponseBody .MinSendable ); amount < minSendable {
451
+ return nil , fmt .Errorf (
452
+ "PayLightningAddress: %w: exceeds minSendable (%f BTC)" ,
453
+ ErrOutsideSendableRange , minSendable ,
454
+ )
455
+ }
456
+
457
+ respData , err = wallet .PostRequest (ctx , "/api/v1/wallet/lnPay" , map [string ]any {
458
+ "amount" : toMillisat (amount ),
459
+ "callback" : lnPayResponseBody .Callback ,
460
+ })
461
+ if err != nil {
462
+ return nil , fmt .Errorf ("PayLightningAddress: %w" , err )
463
+ }
464
+
465
+ var payment Payment
466
+ if err := json .Unmarshal (respData , & payment ); err != nil {
467
+ return nil , fmt .Errorf ("PayLightningAddress: invalid response JSON: %w" , err )
468
+ }
469
+ return & payment , nil
470
+ }
471
+
392
472
// PayVariableInvoice executes a payment to a given variable-amount lightning invoice.
393
473
// The description is stored in the WoS payment history.
394
474
//
0 commit comments