Skip to content

Commit b5b6792

Browse files
committed
WIP: manage sell offers in the negotiator
1 parent 1e1498e commit b5b6792

File tree

7 files changed

+176
-4
lines changed

7 files changed

+176
-4
lines changed

rfqmsg/request.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ type requestMsgData struct {
102102
// ID is the unique identifier of the request for quote (RFQ).
103103
ID ID
104104

105-
// // AssetID represents the identifier of the asset for which the peer
105+
// AssetID represents the identifier of the asset for which the peer
106106
// is requesting a quote.
107107
AssetID *asset.ID
108108

rfqservice/negotiator.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package rfqservice
22

33
import (
4+
"encoding/hex"
45
"fmt"
56
"sync"
67

8+
"github.com/btcsuite/btcd/btcec/v2"
9+
"github.com/lightninglabs/taproot-assets/asset"
710
"github.com/lightninglabs/taproot-assets/fn"
811
"github.com/lightninglabs/taproot-assets/rfqmsg"
912
)
@@ -28,6 +31,12 @@ type Negotiator struct {
2831
// messages.
2932
outgoingMessages chan<- rfqmsg.OutgoingMsg
3033

34+
// assetSellOffers is a map (keyed on asset) that holds the asset sell
35+
// offers that the negotiator has received.
36+
assetSellOffers map[string]AssetSellOffer
37+
38+
assetGroupSellOffers map[string]AssetSellOffer
39+
3140
// ContextGuard provides a wait group and main quit channel that can be
3241
// used to create guarded contexts.
3342
*fn.ContextGuard
@@ -104,6 +113,9 @@ func (n *Negotiator) handlePriceOracleResponse(request rfqmsg.Request,
104113

105114
// HandleIncomingQuoteRequest handles an incoming quote request.
106115
func (n *Negotiator) HandleIncomingQuoteRequest(req rfqmsg.Request) error {
116+
// Ensure that we have a sell offer for the asset that is being
117+
// requested.
118+
107119
// If there is no price oracle available, then we cannot proceed with
108120
// the negotiation. We will reject the quote request with an error.
109121
if n.cfg.PriceOracle == nil {
@@ -141,6 +153,90 @@ func (n *Negotiator) HandleIncomingQuoteRequest(req rfqmsg.Request) error {
141153
return nil
142154
}
143155

156+
// AssetSellOffer is a struct that represents an asset sell offer. This
157+
// data structure describes the maximum amount of an asset that is available
158+
// for sale.
159+
type AssetSellOffer struct {
160+
// AssetID represents the identifier of the subject asset.
161+
AssetID *asset.ID
162+
163+
// AssetGroupKey is the public group key of the subject asset.
164+
AssetGroupKey *btcec.PublicKey
165+
166+
// MaxAssetAmount is the maximum amount of the asset under offer.
167+
MaxAssetAmount uint64
168+
}
169+
170+
// Validate validates the asset sell offer.
171+
func (a *AssetSellOffer) Validate() error {
172+
if a.AssetID == nil && a.AssetGroupKey == nil {
173+
return fmt.Errorf("asset ID is nil and asset group key is nil")
174+
}
175+
176+
if a.AssetID != nil && a.AssetGroupKey != nil {
177+
return fmt.Errorf("asset ID and asset group key are both set")
178+
}
179+
180+
if a.MaxAssetAmount == 0 {
181+
return fmt.Errorf("max asset amount is zero")
182+
}
183+
184+
return nil
185+
}
186+
187+
// UpsertAssetSellOffer upserts an asset sell offer with the negotiator.
188+
func (n *Negotiator) UpsertAssetSellOffer(offer AssetSellOffer) error {
189+
// Validate the offer.
190+
err := offer.Validate()
191+
if err != nil {
192+
return fmt.Errorf("invalid asset sell offer: %w", err)
193+
}
194+
195+
// Store the offer in the appropriate map.
196+
//
197+
// If the asset group key is not nil, then we will use it as the key for
198+
// the offer. Otherwise, we will use the asset ID as the key.
199+
switch {
200+
case offer.AssetGroupKey != nil:
201+
compressedKey := offer.AssetGroupKey.SerializeCompressed()
202+
keyStr := hex.EncodeToString(compressedKey)
203+
204+
n.assetGroupSellOffers[keyStr] = offer
205+
206+
case offer.AssetID != nil:
207+
idStr := offer.AssetID.String()
208+
n.assetSellOffers[idStr] = offer
209+
}
210+
211+
return nil
212+
}
213+
214+
// RemoveAssetSellOffer removes an asset sell offer from the negotiator.
215+
func (n *Negotiator) RemoveAssetSellOffer(assetID *asset.ID,
216+
assetGroupKey *btcec.PublicKey) error {
217+
218+
// Remove the offer from the appropriate map.
219+
//
220+
// If the asset group key is not nil, then we will use it as the key for
221+
// the offer. Otherwise, we will use the asset ID as the key.
222+
switch {
223+
case assetGroupKey != nil:
224+
compressedKey := assetGroupKey.SerializeCompressed()
225+
keyStr := hex.EncodeToString(compressedKey)
226+
227+
delete(n.assetGroupSellOffers, keyStr)
228+
229+
case assetID != nil:
230+
idStr := assetID.String()
231+
delete(n.assetSellOffers, idStr)
232+
233+
default:
234+
return fmt.Errorf("asset ID and asset group key are both nil")
235+
}
236+
237+
return nil
238+
}
239+
144240
// mainEventLoop executes the main event handling loop.
145241
func (n *Negotiator) mainEventLoop() {
146242
log.Debug("Starting negotiator event loop")

rfqservice/rfq.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"sync"
77
"time"
88

9+
"github.com/btcsuite/btcd/btcec/v2"
10+
"github.com/lightninglabs/taproot-assets/asset"
911
"github.com/lightninglabs/taproot-assets/fn"
1012
"github.com/lightninglabs/taproot-assets/rfqmsg"
1113
)
@@ -289,3 +291,27 @@ func (m *Manager) mainEventLoop() {
289291
}
290292
}
291293
}
294+
295+
// UpsertAssetSellOffer upserts an asset sell offer with the RFQ manager.
296+
func (m *Manager) UpsertAssetSellOffer(offer AssetSellOffer) error {
297+
// Store the asset sell offer in the negotiator.
298+
err := m.negotiator.UpsertAssetSellOffer(offer)
299+
if err != nil {
300+
return fmt.Errorf("error registering asset sell offer: %w", err)
301+
}
302+
303+
return nil
304+
}
305+
306+
// RemoveAssetSellOffer removes an asset sell offer from the RFQ manager.
307+
func (m *Manager) RemoveAssetSellOffer(assetID *asset.ID,
308+
assetGroupKey *btcec.PublicKey) error {
309+
310+
// Remove the asset sell offer from the negotiator.
311+
err := m.negotiator.RemoveAssetSellOffer(assetID, assetGroupKey)
312+
if err != nil {
313+
return fmt.Errorf("error removing asset sell offer: %w", err)
314+
}
315+
316+
return nil
317+
}

rfqservice/stream.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,6 @@ func (h *StreamHandler) handleIncomingQuoteRequest(
8989
"from a wire message: %w", err)
9090
}
9191

92-
// TODO(ffranr): Determine whether to keep or discard the RFQ message
93-
// based on the peer's ID and the asset's ID.
94-
9592
// Send the quote request to the RFQ manager.
9693
var msg rfqmsg.IncomingMsg = quoteRequest
9794
sendSuccess := fn.SendOrQuit(h.incomingMessages, msg, h.Quit)

rpcserver.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4510,6 +4510,49 @@ func (r *rpcServer) RemoveUTXOLease(ctx context.Context,
45104510
return &wrpc.RemoveUTXOLeaseResponse{}, nil
45114511
}
45124512

4513+
//func (r *rpcServer) UpsertAssetSellOffer(ctx context.Context,
4514+
// in *taprpc.BurnAssetRequest) (*taprpc.BurnAssetResponse, error) {
4515+
//
4516+
// var assetID asset.ID
4517+
// switch {
4518+
// case len(in.GetAssetId()) > 0:
4519+
// copy(assetID[:], in.GetAssetId())
4520+
//
4521+
// case len(in.GetAssetIdStr()) > 0:
4522+
// assetIDBytes, err := hex.DecodeString(in.GetAssetIdStr())
4523+
// if err != nil {
4524+
// return nil, fmt.Errorf("error decoding asset ID: %w",
4525+
// err)
4526+
// }
4527+
//
4528+
// copy(assetID[:], assetIDBytes)
4529+
//
4530+
// default:
4531+
// return nil, fmt.Errorf("asset ID must be specified")
4532+
// }
4533+
//
4534+
// var groupKey *btcec.PublicKey
4535+
// assetGroup, err := r.cfg.TapAddrBook.QueryAssetGroup(ctx, assetID)
4536+
// switch {
4537+
// case err == nil && assetGroup.GroupKey != nil:
4538+
// // We found the asset group, so we can use the group key to
4539+
// // burn the asset.
4540+
// groupKey = &assetGroup.GroupPubKey
4541+
// case errors.Is(err, address.ErrAssetGroupUnknown):
4542+
// // We don't know the asset group, so we'll try to burn the
4543+
// // asset using the asset ID only.
4544+
// rpcsLog.Debug("Asset group key not found, asset may not be " +
4545+
// "part of a group")
4546+
// case err != nil:
4547+
// return nil, fmt.Errorf("error querying asset group: %w", err)
4548+
// }
4549+
//
4550+
// var serializedGroupKey []byte
4551+
// if groupKey != nil {
4552+
// serializedGroupKey = groupKey.SerializeCompressed()
4553+
// }
4554+
//}
4555+
45134556
// MarshalAssetFedSyncCfg returns an RPC ready asset specific federation sync
45144557
// config.
45154558
func MarshalAssetFedSyncCfg(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DROP TABLE IF EXISTS asset_sell_offers;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- asset_sell_offers is a table that stores the set of all asset sell offers
2+
-- that are applicable to the RFQ negotiation process.
3+
CREATE TABLE IF NOT EXISTS asset_sell_offers (
4+
txn_id BIGINT PRIMARY KEY,
5+
6+
asset_id BIGINT REFERENCES assets(asset_id),
7+
8+
max_amount BIGINT NOT NULL
9+
);

0 commit comments

Comments
 (0)