Skip to content

Conversation

@ffranr
Copy link
Contributor

@ffranr ffranr commented Dec 9, 2025

  • Add InternalPortfolioPilot with config (price oracle, tolerance, peer forwarding) and ResolveBuyRequest.
  • Wire negotiator/manager to default to the internal pilot; keep sell path on existing oracle flow for now.
  • InternalPortfolioPilot enforces rate tolerance using the configured AcceptPriceDeviationPpm and optional peer forwarding to oracle.

@gemini-code-assist
Copy link

Summary of Changes

Hello @ffranr, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new architectural component, the PortfolioPilot, designed to encapsulate and manage the decision-making logic for Request for Quote (RFQ) requests. Specifically, it implements an InternalPortfolioPilot that handles incoming buy requests by querying a price oracle, enforcing price deviation tolerances, and deciding whether to accept or reject the quote. This change modularizes the RFQ process, making it more extensible for future financial decision-making strategies, while currently focusing only on the buy-side of transactions.

Highlights

  • Introduction of PortfolioPilot: A new PortfolioPilot interface and its default implementation, InternalPortfolioPilot, have been added to centralize and manage the decision-making logic for Request for Quote (RFQ) requests.
  • Buy Request Resolution Logic: The HandleIncomingBuyRequest method in the Negotiator now delegates its decision-making process to the PortfolioPilot, which encapsulates the logic for price oracle queries, tolerance checks, and ultimately deciding whether to accept or reject a buy request.
  • Configuration and Wiring: The Manager and Negotiator components have been updated to include and utilize the PortfolioPilot. The InternalPortfolioPilot is automatically initialized and used as the default if no specific PortfolioPilot is provided in the configuration.
  • Initial Buy-Side Focus: The current implementation of the InternalPortfolioPilot is specifically designed to handle incoming buy requests. Sell requests will continue to be processed through the existing price oracle flow for now.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a PortfolioPilot interface to abstract the RFQ decision logic, with a default InternalPortfolioPilot implementation. This is a positive change for modularity. My review focuses on several critical correctness issues in the new implementation, including an interface signature mismatch, an empty error handling block, and incorrect usage of the fn.Either type which would prevent compilation. I've also provided suggestions for a few medium-severity issues like an unused import and a redundant function parameter to improve code quality.

Comment on lines 373 to 431
result, err := n.cfg.PortfolioPilot.ResolveBuyRequest(
ctx, request,
)
if err != nil {
// Send a reject message to the peer.

}

if result.IsErr() {
n.cfg.ErrChan <- fmt.Errorf("resolve buy request: %w",
result.Err())

msg := rfqmsg.NewReject(
request.Peer, request.ID,
rfqmsg.ErrUnknownReject,
request.Peer, request.ID, rfqmsg.ErrUnknownReject,
)
sendOutgoingMsg(msg)
return
}

// Add an error to the error channel and return.
err = fmt.Errorf("failed to query sell price from "+
"oracle: %w", err)
outcome, unpackErr := result.Unpack()
if unpackErr != nil {
n.cfg.ErrChan <- fmt.Errorf("resolve buy request: %w",
unpackErr)

msg := rfqmsg.NewReject(
request.Peer, request.ID, rfqmsg.ErrUnknownReject,
)
sendOutgoingMsg(msg)
return
}

if outcome.IsRight() {
reason := rfqmsg.ErrUnknownReject
outcome.WhenRight(func(err rfqmsg.RejectErr) {
reason = err
})

msg := rfqmsg.NewReject(
request.Peer, request.ID, reason,
)
sendOutgoingMsg(msg)
return
}

var assetRate *rfqmsg.AssetRate
outcome.WhenLeft(func(rate rfqmsg.AssetRate) {
assetRate = &rate
})
if assetRate == nil {
err := fmt.Errorf("resolve buy request: missing " +
"asset rate on accept decision")
n.cfg.ErrChan <- err

msg := rfqmsg.NewReject(
request.Peer, request.ID,
rfqmsg.ErrUnknownReject,
)
sendOutgoingMsg(msg)
return
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

This block has several critical issues:

  1. The if err != nil block is empty, which means any internal errors from ResolveBuyRequest are silently ignored.
  2. The code attempts to use methods like IsErr(), Err(), and Unpack() on the result variable, which is of type fn.Either. These methods do not exist on fn.Either, which will cause a compilation error.

The logic for handling the (fn.Either, error) return from ResolveBuyRequest should be corrected to properly handle internal errors and correctly unpack the Either type to process accept/reject decisions.

result, err := n.cfg.PortfolioPilot.ResolveBuyRequest(
	ctx, request,
)
if err != nil {
	// This is an internal error that occurred within the
	// portfolio pilot. We'll send a generic rejection to
	// the peer and log the actual error.
	n.cfg.ErrChan <- fmt.Errorf("resolve buy request: %w", err)

	msg := rfqmsg.NewReject(
		request.Peer, request.ID, rfqmsg.ErrUnknownReject,
	)
	sendOutgoingMsg(msg)
	return
}

// The portfolio pilot has made a decision. This can either be to
// accept the request with a given asset rate, or to reject it
// with a specific error.
var assetRate *rfqmsg.AssetRate
result.WhenLeft(func(rate rfqmsg.AssetRate) {
	assetRate = &rate
})
result.WhenRight(func(rejectErr rfqmsg.RejectErr) {
	msg := rfqmsg.NewReject(
		request.Peer, request.ID, rejectErr,
	)
	sendOutgoingMsg(msg)
})

// If we have a rejection, we'll exit now.
if result.IsRight() {
	return
}

// If we don't have an asset rate, then something has gone
// wrong.
if assetRate == nil {
	err := fmt.Errorf("resolve buy request: missing " +
		"asset rate on accept decision")
	n.cfg.ErrChan <- err

	msg := rfqmsg.NewReject(
		request.Peer, request.ID,
		rfqmsg.ErrUnknownReject,
	)
	sendOutgoingMsg(msg)
	return
}

type PortfolioPilot interface {
// ResolveBuyRequest applies the portfolio pilot's decision logic for a
// BuyRequest.
ResolveBuyRequest(context.Context, rfqmsg.BuyRequest) ResolveRequest

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The signature of ResolveBuyRequest in the PortfolioPilot interface does not match its implementation in InternalPortfolioPilot. The implementation returns (ResolveRequest, error), but the interface only specifies ResolveRequest. The caller in negotiator.go expects the (ResolveRequest, error) signature. To fix this, the interface definition should be updated to match the implementation.

Suggested change
ResolveBuyRequest(context.Context, rfqmsg.BuyRequest) ResolveRequest
ResolveBuyRequest(context.Context, rfqmsg.BuyRequest) (ResolveRequest, error)

"github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightninglabs/taproot-assets/rfqmsg"
lfn "github.com/lightningnetwork/lnd/fn/v2"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The imported package lfn is not used in this file and should be removed.

cfg.PortfolioPilot = NewInternalPortfolioPilot(
cfg.PriceOracle, cfgPortfolioPilot,
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The NewInternalPortfolioPilot function is being updated to remove a redundant parameter. The call to it here should be updated accordingly by removing the first argument.

Suggested change
cfg.PortfolioPilot = NewInternalPortfolioPilot(
cfg.PriceOracle, cfgPortfolioPilot,
)
cfg.PortfolioPilot = NewInternalPortfolioPilot(
cfgPortfolioPilot,
)

Comment on lines 46 to 48
func NewInternalPortfolioPilot(_ PriceOracle,
cfg InternalPortfolioPilotConfig) *InternalPortfolioPilot {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The first parameter _ PriceOracle is unused within this function. The PriceOracle is already available in the cfg struct. This redundant parameter should be removed for clarity. The call site in negotiator.go will also need to be updated.

func NewInternalPortfolioPilot(cfg InternalPortfolioPilotConfig) *InternalPortfolioPilot {

Replace direct goroutine spawning and WaitGroup usage with a utility
wrapper for error handling. This improves code clarity and error
propagation.
@ffranr ffranr force-pushed the wip/portfolio-pilot/add-ResolveBuyRequest branch 2 times, most recently from 1ba9497 to 5f52053 Compare December 9, 2025 19:42
@coveralls
Copy link

coveralls commented Dec 9, 2025

Pull Request Test Coverage Report for Build 20083463873

Details

  • 60 of 103 (58.25%) changed or added relevant lines in 3 files are covered.
  • 8302 unchanged lines in 123 files lost coverage.
  • Overall coverage decreased (-7.0%) to 49.772%

Changes Missing Coverage Covered Lines Changed/Added Lines %
rfq/negotiator.go 24 40 60.0%
rfq/portfolio_pilot.go 35 62 56.45%
Files with Coverage Reduction New Missed Lines %
fn/context_guard.go 1 91.94%
authmailbox/client.go 2 66.67%
commitment/proof.go 2 87.29%
fn/retry.go 2 92.5%
tapdb/interfaces.go 2 78.33%
tapdb/sqlc/transfers.sql.go 2 83.33%
universe/interface.go 3 74.21%
commitment/encoding.go 4 68.75%
mssmt/encoding.go 4 76.67%
rpcutils/price_oracle_marshal.go 4 85.07%
Totals Coverage Status
Change from base Build 20053123061: -7.0%
Covered Lines: 57097
Relevant Lines: 114718

💛 - Coveralls

- Add InternalPortfolioPilot class.
- Support resolving buy requests only.
  Additional functionality will be implemented later.
- Add PortfolioPilot to Negotiator and Manager structs.
- Use internal implementation of PortfolioPilot if not specified.
- Remove HasAssetSellOffer method.
- Delegate buy quote request handling to PortfolioPilot.
- Improve error management and response handling.
@ffranr ffranr force-pushed the wip/portfolio-pilot/add-ResolveBuyRequest branch from 5f52053 to 7fea652 Compare December 10, 2025 00:50
@ffranr ffranr closed this Dec 11, 2025
@github-project-automation github-project-automation bot moved this from 🆕 New to ✅ Done in Taproot-Assets Project Board Dec 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

3 participants