diff --git a/.gitignore b/.gitignore index e6cc0d1b4..9ed1ba78b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ cmd/tapd/tapd /itest/custom_channels/*.log.* /docs/examples/basic-price-oracle/basic-price-oracle +/docs/examples/basic-portfolio-pilot/basic-portfolio-pilot +basic-portfolio-pilot # Test binary, built with `go test -c` *.test diff --git a/docs/examples/basic-portfolio-pilot/main.go b/docs/examples/basic-portfolio-pilot/main.go index a35de9f5a..898f1f715 100644 --- a/docs/examples/basic-portfolio-pilot/main.go +++ b/docs/examples/basic-portfolio-pilot/main.go @@ -104,9 +104,19 @@ func (p *RpcPortfolioPilotServer) ResolveRequest(_ context.Context, var hint *portfoliopilotrpc.AssetRate switch r := req.GetRequest().(type) { case *portfoliopilotrpc.ResolveRequestRequest_BuyRequest: - hint = r.BuyRequest.GetAssetRateHint() + br := r.BuyRequest + hint = br.GetAssetRateHint() + log.Printf("ResolveRequest buy: max=%d min=%d "+ + "rate_limit=%v", br.GetAssetMaxAmount(), + br.GetAssetMinAmount(), + br.GetAssetRateLimit()) case *portfoliopilotrpc.ResolveRequestRequest_SellRequest: - hint = r.SellRequest.GetAssetRateHint() + sr := r.SellRequest + hint = sr.GetAssetRateHint() + log.Printf("ResolveRequest sell: max=%d min=%d "+ + "rate_limit=%v", sr.GetPaymentMaxAmount(), + sr.GetPaymentMinAmount(), + sr.GetAssetRateLimit()) default: return nil, fmt.Errorf("unknown request type: %T", r) } diff --git a/docs/release-notes/release-notes-0.8.0.md b/docs/release-notes/release-notes-0.8.0.md index 2221af873..ea9f076ef 100644 --- a/docs/release-notes/release-notes-0.8.0.md +++ b/docs/release-notes/release-notes-0.8.0.md @@ -103,6 +103,22 @@ **Note:** For full functionality, it is highly recommended to start LND with the `--store-final-htlc-resolutions` flag enabled, which is disabled by default. +- [Limit-Order Constraints](https://github.com/lightninglabs/taproot-assets/pull/2048): + RFQ buy and sell orders can now carry explicit limit-price bounds + (`asset_rate_limit`) and minimum fill sizes (`asset_min_amt` / + `payment_min_amt`). Quotes that violate these constraints are rejected + with machine-readable reasons (`RATE_BOUND_MISS`, `MIN_FILL_NOT_MET`). + New fields are optional and backward-compatible; constraint validation + only activates when they are present. + +- [Execution Policy](https://github.com/lightninglabs/taproot-assets/pull/2049): + RFQ buy and sell orders can now specify an execution policy: IOC + (Immediate-Or-Cancel, the default) allows partial fills above the + minimum, while FOK (Fill-Or-Kill) requires the full requested amount + or rejects the quote. FOK viability is checked in `VerifyAcceptQuote` + with a new `FOK_NOT_VIABLE` reject code. New fields are optional and + backward-compatible. + ## Functional Enhancements - [Wallet Backup/Restore](https://github.com/lightninglabs/taproot-assets/pull/1980): @@ -148,6 +164,19 @@ Add `RemoveMessage` RPC to the auth mailbox service. Receivers can authenticate with a Schnorr signature to delete their own messages by ID. +- [PR#2048](https://github.com/lightninglabs/taproot-assets/pull/2048): + Add `asset_rate_limit` to `AddAssetBuyOrder` and `AddAssetSellOrder` + requests. Add `asset_min_amt` to buy orders and `payment_min_amt` + to sell orders. Add `asset_rate_limit` and min fill fields to + `PortfolioPilot.ResolveRequest` for constraint forwarding. Add + `RATE_BOUND_MISS` and `MIN_FILL_NOT_MET` to `QuoteRespStatus`. + +- [PR#2049](https://github.com/lightninglabs/taproot-assets/pull/2049): + Add `execution_policy` enum (`EXECUTION_POLICY_IOC`, + `EXECUTION_POLICY_FOK`) to `AddAssetBuyOrder` and `AddAssetSellOrder` + requests, and to `PortfolioPilot.ResolveRequest` for constraint + forwarding. Add `FOK_NOT_VIABLE` to `QuoteRespStatus`. + ## tapcli Additions - [Wallet Backup CLI](https://github.com/lightninglabs/taproot-assets/pull/1980): @@ -331,6 +360,13 @@ New integration test `testForwardingEventHistory` verifies that forwarding events are properly logged when routing asset payments. +- [PR#2048](https://github.com/lightninglabs/taproot-assets/pull/2048): + Add unit, property-based, and integration tests for limit-order + constraint fields. + +- [PR#2049](https://github.com/lightninglabs/taproot-assets/pull/2049): + Add unit, property-based, and integration tests for execution policy. + ## Database - [forwards table](https://github.com/lightninglabs/taproot-assets/pull/1921): diff --git a/itest/custom_channels/custom_channels_test.go b/itest/custom_channels/custom_channels_test.go index 00614d383..292ab4c78 100644 --- a/itest/custom_channels/custom_channels_test.go +++ b/itest/custom_channels/custom_channels_test.go @@ -99,6 +99,10 @@ var testCases = []*ccTestCase{ name: "invoice quote expiry mismatch", test: testCustomChannelsInvoiceQuoteExpiryMismatch, }, + { + name: "limit constraints", + test: testCustomChannelsLimitConstraints, + }, { name: "fee", test: testCustomChannelsFee, diff --git a/itest/custom_channels/limit_constraints_test.go b/itest/custom_channels/limit_constraints_test.go new file mode 100644 index 000000000..03ad35c52 --- /dev/null +++ b/itest/custom_channels/limit_constraints_test.go @@ -0,0 +1,309 @@ +//go:build itest + +package custom_channels + +import ( + "context" + "fmt" + "math" + "math/big" + "slices" + "time" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/itest" + "github.com/lightninglabs/taproot-assets/proof" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightninglabs/taproot-assets/rfqmsg" + "github.com/lightninglabs/taproot-assets/taprpc" + "github.com/lightninglabs/taproot-assets/taprpc/mintrpc" + "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + tchrpc "github.com/lightninglabs/taproot-assets/taprpc/tapchannelrpc" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lntest" + "github.com/lightningnetwork/lnd/lntest/node" + "github.com/lightningnetwork/lnd/lntest/port" + "github.com/stretchr/testify/require" +) + +// testCustomChannelsLimitConstraints verifies that RFQ limit-order +// constraints (asset_rate_limit, payment_min_amt) work correctly in +// the context of real asset channels. It negotiates a sell quote with +// satisfied constraints, then sends a payment using that quote. +// +//nolint:lll +func testCustomChannelsLimitConstraints(_ context.Context, + net *itest.IntegratedNetworkHarness, t *ccHarnessTest) { + + usdMetaData := &taprpc.AssetMeta{ + Data: []byte(`{ +"description":"USD stablecoin for limit constraint test" +}`), + Type: taprpc.AssetMetaType_META_TYPE_JSON, + } + + const decimalDisplay = 6 + tcAsset := &mintrpc.MintAsset{ + AssetType: taprpc.AssetType_NORMAL, + Name: "USD-limits", + AssetMeta: usdMetaData, + Amount: 1_000_000_000_000, + DecimalDisplay: decimalDisplay, + } + + oracleAddr := fmt.Sprintf( + "localhost:%d", port.NextAvailablePort(), + ) + oracle := itest.NewOracleHarness(oracleAddr) + oracle.Start(t.t) + t.t.Cleanup(oracle.Stop) + + lndArgs := slices.Clone(lndArgsTemplate) + tapdArgs := slices.Clone(tapdArgsTemplateNoOracle) + tapdArgs = append(tapdArgs, fmt.Sprintf( + "--experimental.rfq.priceoracleaddress=rfqrpc://%s", + oracleAddr, + )) + tapdArgs = append( + tapdArgs, + "--experimental.rfq.priceoracletlsinsecure", + ) + + charliePort := port.NextAvailablePort() + tapdArgs = append(tapdArgs, fmt.Sprintf( + "--proofcourieraddr=%s://%s", + proof.UniverseRpcCourierType, + fmt.Sprintf(node.ListenerFormat, charliePort), + )) + + // Topology: Charlie --[assets]--> Dave --[sats]--> Erin + charlieLndArgs := slices.Clone(lndArgs) + charlieLndArgs = append(charlieLndArgs, fmt.Sprintf( + "--rpclisten=127.0.0.1:%d", charliePort, + )) + charlie := net.NewNode("Charlie", charlieLndArgs, tapdArgs) + dave := net.NewNode("Dave", lndArgs, tapdArgs) + erin := net.NewNode("Erin", lndArgs, tapdArgs) + + nodes := []*itest.IntegratedNode{charlie, dave, erin} + connectAllNodes(t.t, net, nodes) + fundAllNodes(t.t, net, nodes) + + // Open a normal BTC channel between Dave and Erin. + const btcChannelFundingAmount = 10_000_000 + chanPointDE := openChannelAndAssert( + t, net, dave, erin, lntest.OpenChannelParams{ + Amt: btcChannelFundingAmount, + SatPerVByte: 5, + }, + ) + defer closeChannelAndAssert(t, net, dave, chanPointDE, false) + + assertChannelKnown(t.t, charlie, chanPointDE) + + // Mint on Charlie. + mintedAssets := itest.MintAssetsConfirmBatch( + t.t, net.Miner.Client, asTapd(charlie), + []*mintrpc.MintAssetRequest{ + {Asset: tcAsset}, + }, + ) + usdAsset := mintedAssets[0] + assetID := usdAsset.AssetGenesis.AssetId + + var id asset.ID + copy(id[:], assetID) + + // Oracle price: ~66,548.40 USD/BTC with decimal display 6. + salePrice := rfqmath.NewBigIntFixedPoint(65_217_43, 2) + purchasePrice := rfqmath.NewBigIntFixedPoint(67_879_37, 2) + factor := rfqmath.NewBigInt( + big.NewInt(int64(math.Pow10(decimalDisplay))), + ) + salePrice.Coefficient = salePrice.Coefficient.Mul(factor) + purchasePrice.Coefficient = purchasePrice.Coefficient.Mul( + factor, + ) + oracle.SetPrice( + asset.NewSpecifierFromId(id), purchasePrice, salePrice, + ) + + t.Logf("Syncing universes...") + syncUniverses(t.t, charlie, dave, erin) + + // Send assets to Dave so he has a balance. + const sendAmount = uint64(400_000_000) + charlieFundingAmount := usdAsset.Amount - sendAmount + + ctxb := context.Background() + daveAddr, err := dave.NewAddr(ctxb, &taprpc.NewAddrRequest{ + Amt: sendAmount, + AssetId: assetID, + ProofCourierAddr: fmt.Sprintf( + "%s://%s", proof.UniverseRpcCourierType, + charlie.RPCAddr(), + ), + }) + require.NoError(t.t, err) + + sendResp, err := charlie.SendAsset( + ctxb, &taprpc.SendAssetRequest{ + TapAddrs: []string{daveAddr.Encoded}, + }, + ) + require.NoError(t.t, err) + itest.ConfirmAndAssertOutboundTransfer( + t.t, net.Miner.Client, asTapd(charlie), sendResp, + assetID, + []uint64{usdAsset.Amount - sendAmount, sendAmount}, + 0, 1, + ) + itest.AssertNonInteractiveRecvComplete(t.t, asTapd(dave), 1) + + // Open asset channel Charlie → Dave. + t.Logf("Opening asset channel Charlie → Dave...") + net.EnsureConnected(t.t, charlie, dave) + fundResp, err := charlie.FundChannel( + ctxb, &tchrpc.FundChannelRequest{ + AssetAmount: charlieFundingAmount, + AssetId: assetID, + PeerPubkey: dave.PubKey[:], + FeeRateSatPerVbyte: 5, + PushSat: DefaultPushSat, + }, + ) + require.NoError(t.t, err) + t.Logf("Funded channel: %v", fundResp) + + mineBlocks(t, net, 6, 1) + + chanPointCD := &lnrpc.ChannelPoint{ + OutputIndex: uint32(fundResp.OutputIndex), + FundingTxid: &lnrpc.ChannelPoint_FundingTxidStr{ + FundingTxidStr: fundResp.Txid, + }, + } + + logBalance(t.t, nodes, assetID, "after channel open") + + // ----------------------------------------------------------------- + // Negotiate a sell order from Charlie with constraints. + // Rate limit is set well above the oracle rate (ceiling for + // sell), so the constraint is satisfied. + // ----------------------------------------------------------------- + t.Logf("Negotiating sell order with constraints...") + + inOneHour := time.Now().Add(time.Hour) + sellResp, err := asTapd(charlie).RfqClient.AddAssetSellOrder( + ctxb, &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + }, + }, + PaymentMaxAmt: 180_000_000, + AssetRateLimit: &rfqrpc.FixedPoint{ + // Ceiling well above oracle rate. + Coefficient: "100000000000000", + Scale: 2, + }, + Expiry: uint64(inOneHour.Unix()), + PeerPubKey: dave.PubKey[:], + TimeoutSeconds: 10, + }, + ) + require.NoError(t.t, err, "sell order with constraints") + + accepted := sellResp.GetAcceptedQuote() + require.NotNil(t.t, accepted, "expected accepted sell quote") + t.Logf("Sell quote accepted: scid=%d", accepted.Scid) + + // ----------------------------------------------------------------- + // Pay an invoice using the pre-negotiated quote. + // ----------------------------------------------------------------- + t.Logf("Paying invoice with constrained quote...") + + // Erin has no asset channel, so we create a regular BTC + // invoice. Charlie pays it with assets via the sell quote. + const invoiceMsat = 100_000_000 // 100K sats + invoiceResp, err := erin.LightningClient.AddInvoice( + ctxb, &lnrpc.Invoice{ + ValueMsat: invoiceMsat, + }, + ) + require.NoError(t.t, err) + + var quoteID rfqmsg.ID + copy(quoteID[:], accepted.Id) + + numUnits, _ := payInvoiceWithAssets( + t.t, charlie, dave, + invoiceResp.PaymentRequest, + assetID, withRFQ(quoteID), + ) + require.Greater(t.t, numUnits, uint64(0)) + + logBalance(t.t, nodes, assetID, "after payment") + t.Logf("Payment completed: %d asset units sent", numUnits) + + // ----------------------------------------------------------------- + // Negotiate a sell order from Charlie with FOK policy. + // Rate limit ceiling is generous and the payment max is + // large enough that FOK conversion yields non-zero units. + // ----------------------------------------------------------------- + t.Logf("Negotiating sell order with FOK policy...") + + sellRespFOK, err := asTapd(charlie).RfqClient.AddAssetSellOrder( + ctxb, &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: assetID, + }, + }, + PaymentMaxAmt: 180_000_000, + AssetRateLimit: &rfqrpc.FixedPoint{ + Coefficient: "100000000000000", + Scale: 2, + }, + ExecutionPolicy: rfqrpc.ExecutionPolicy_EXECUTION_POLICY_FOK, + Expiry: uint64(inOneHour.Unix()), + PeerPubKey: dave.PubKey[:], + TimeoutSeconds: 10, + }, + ) + require.NoError(t.t, err, "sell order with FOK policy") + + acceptedFOK := sellRespFOK.GetAcceptedQuote() + require.NotNil( + t.t, acceptedFOK, "expected accepted FOK sell quote", + ) + t.Logf("FOK sell quote accepted: scid=%d", acceptedFOK.Scid) + + // Pay using the FOK quote with a regular BTC invoice. + invoiceRespFOK, err := erin.LightningClient.AddInvoice( + ctxb, &lnrpc.Invoice{ + ValueMsat: invoiceMsat, + }, + ) + require.NoError(t.t, err) + + var quoteIDFOK rfqmsg.ID + copy(quoteIDFOK[:], acceptedFOK.Id) + + numUnitsFOK, _ := payInvoiceWithAssets( + t.t, charlie, dave, + invoiceRespFOK.PaymentRequest, + assetID, withRFQ(quoteIDFOK), + ) + require.Greater(t.t, numUnitsFOK, uint64(0)) + + logBalance(t.t, nodes, assetID, "after FOK payment") + t.Logf("FOK payment completed: %d units sent", numUnitsFOK) + + // Close channels. + closeAssetChannelAndAssert( + t, net, charlie, dave, chanPointCD, + [][]byte{assetID}, nil, charlie, + noOpCoOpCloseBalanceCheck, + ) +} diff --git a/itest/portfolio_pilot_harness.go b/itest/portfolio_pilot_harness.go index ec23785c9..2f28d9d7e 100644 --- a/itest/portfolio_pilot_harness.go +++ b/itest/portfolio_pilot_harness.go @@ -149,9 +149,19 @@ func (p *portfolioPilotHarness) ResolveRequest(_ context.Context, var hint *pilotrpc.AssetRate switch r := req.GetRequest().(type) { case *pilotrpc.ResolveRequestRequest_BuyRequest: - hint = r.BuyRequest.GetAssetRateHint() + br := r.BuyRequest + hint = br.GetAssetRateHint() + log.Infof("ResolveRequest buy: max=%d min=%d "+ + "rate_limit=%v", br.GetAssetMaxAmount(), + br.GetAssetMinAmount(), + br.GetAssetRateLimit()) case *pilotrpc.ResolveRequestRequest_SellRequest: - hint = r.SellRequest.GetAssetRateHint() + sr := r.SellRequest + hint = sr.GetAssetRateHint() + log.Infof("ResolveRequest sell: max=%d min=%d "+ + "rate_limit=%v", sr.GetPaymentMaxAmount(), + sr.GetPaymentMinAmount(), + sr.GetAssetRateLimit()) default: return nil, fmt.Errorf("unknown request type: %T", r) } diff --git a/itest/rfq_test.go b/itest/rfq_test.go index e38f6bc45..78b651b3e 100644 --- a/itest/rfq_test.go +++ b/itest/rfq_test.go @@ -844,6 +844,246 @@ func testRfqPortfolioPilotRpc(t *harnessTest) { }, rfqTimeout, 50*time.Millisecond) } +// testRfqLimitConstraints tests that RFQ negotiation correctly enforces +// limit-order constraints (asset_rate_limit, asset_min_amt, +// payment_min_amt) at the RPC layer. It uses the oracle harness with +// SetPrice for deterministic rates and the standard 3-node topology. +func testRfqLimitConstraints(t *harnessTest) { + // Start a mock price oracle RPC server. + oracleAddr := fmt.Sprintf("127.0.0.1:%d", port.NextAvailablePort()) + oracle := NewOracleHarness(oracleAddr) + oracle.Start(t.t) + t.t.Cleanup(oracle.Stop) + + oracleURL := fmt.Sprintf("rfqrpc://%s", oracleAddr) + + // Initialize the standard 3-node test scenario. + ts := newRfqTestScenario(t, WithRfqOracleServer(oracleURL)) + + // Mint an asset with Bob's tapd node. + rpcAssets := MintAssetsConfirmBatch( + t.t, t.lndHarness.Miner().Client, ts.BobTapd, + []*mintrpc.MintAssetRequest{issuableAssets[0]}, + ) + mintedAssetId := rpcAssets[0].AssetGenesis.AssetId + + var assetID asset.ID + copy(assetID[:], mintedAssetId) + specifier := asset.NewSpecifierFromId(assetID) + + // Set oracle rate to 1000 asset units per BTC (coeff=1000000, + // scale=3). Use same price for both bid and ask (no spread). + oracleRate := rfqmath.NewBigIntFixedPoint(1000_000, 3) + oracle.SetPrice(specifier, oracleRate, oracleRate) + + ctx := context.Background() + + // Bob registers a sell offer so Carol can buy. + _, err := ts.BobTapd.AddAssetSellOffer( + ctx, &rfqrpc.AddAssetSellOfferRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + MaxUnits: 1000, + }, + ) + require.NoError(t.t, err) + + // Bob also registers a buy offer so Alice can sell. + _, err = ts.BobTapd.AddAssetBuyOffer( + ctx, &rfqrpc.AddAssetBuyOfferRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + MaxUnits: 1000, + }, + ) + require.NoError(t.t, err) + + expiry := uint64(time.Now().Add(24 * time.Hour).Unix()) + + // ----------------------------------------------------------------- + // Sub-test 1: Buy with satisfied rate limit. + // + // Oracle rate = 1000. Carol sets rate limit = 500 (floor for + // buy). Since 1000 >= 500, the constraint passes. + // ----------------------------------------------------------------- + t.Log("Sub-test 1: buy with satisfied rate limit") + + carolEvents, err := ts.CarolTapd.SubscribeRfqEventNtfns( + ctx, &rfqrpc.SubscribeRfqEventNtfnsRequest{}, + ) + require.NoError(t.t, err) + + buyReq := &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + AssetMaxAmt: 6, + AssetMinAmt: 1, + AssetRateLimit: &rfqrpc.FixedPoint{ + Coefficient: "500000", + Scale: 3, + }, + Expiry: expiry, + PeerPubKey: ts.BobLnd.PubKey[:], + TimeoutSeconds: uint32(rfqTimeout.Seconds()), + SkipAssetChannelCheck: true, + } + _, err = ts.CarolTapd.AddAssetBuyOrder(ctx, buyReq) + require.NoError(t.t, err, "buy with satisfied rate limit") + + BeforeTimeout(t.t, func() { + event, err := carolEvents.Recv() + require.NoError(t.t, err) + + _, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedBuyQuote) + require.True(t.t, ok, "expected PeerAcceptedBuyQuote, "+ + "got: %v", event) + }, rfqTimeout) + + err = carolEvents.CloseSend() + require.NoError(t.t, err) + + // ----------------------------------------------------------------- + // Sub-test 2: Buy with violated rate limit. + // + // Oracle rate = 1000. Carol sets rate limit = 2000 (floor for + // buy). Since 1000 < 2000, the rate bound check fails. + // ----------------------------------------------------------------- + t.Log("Sub-test 2: buy with violated rate limit") + + buyReq2 := &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + AssetMaxAmt: 6, + AssetRateLimit: &rfqrpc.FixedPoint{ + Coefficient: "2000000", + Scale: 3, + }, + Expiry: expiry, + PeerPubKey: ts.BobLnd.PubKey[:], + TimeoutSeconds: uint32(rfqTimeout.Seconds()), + SkipAssetChannelCheck: true, + } + _, err = ts.CarolTapd.AddAssetBuyOrder(ctx, buyReq2) + require.ErrorContains( + t.t, err, "rejected quote", + "expected rate bound rejection for buy order", + ) + + // ----------------------------------------------------------------- + // Sub-test 3: Sell with satisfied constraints. + // + // Oracle rate = 1000. Alice sets rate limit = 2000 (ceiling + // for sell). Since 1000 <= 2000, the constraint passes. + // ----------------------------------------------------------------- + t.Log("Sub-test 3: sell with satisfied constraints") + + aliceEvents, err := ts.AliceTapd.SubscribeRfqEventNtfns( + ctx, &rfqrpc.SubscribeRfqEventNtfnsRequest{}, + ) + require.NoError(t.t, err) + + sellReq := &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + PaymentMaxAmt: 42000, + AssetRateLimit: &rfqrpc.FixedPoint{ + Coefficient: "2000000", + Scale: 3, + }, + Expiry: expiry, + PeerPubKey: ts.BobLnd.PubKey[:], + TimeoutSeconds: uint32(rfqTimeout.Seconds()), + SkipAssetChannelCheck: true, + } + _, err = ts.AliceTapd.AddAssetSellOrder(ctx, sellReq) + require.NoError(t.t, err, "sell with satisfied constraints") + + BeforeTimeout(t.t, func() { + event, err := aliceEvents.Recv() + require.NoError(t.t, err) + + _, ok := event.Event.(*rfqrpc.RfqEvent_PeerAcceptedSellQuote) + require.True(t.t, ok, "expected PeerAcceptedSellQuote, "+ + "got: %v", event) + }, rfqTimeout) + + err = aliceEvents.CloseSend() + require.NoError(t.t, err) + + // ----------------------------------------------------------------- + // Sub-test 4: Sell with violated rate limit. + // + // Oracle rate = 1000. Alice sets rate limit = 500 (ceiling for + // sell). Since 1000 > 500, the rate bound check fails. + // ----------------------------------------------------------------- + t.Log("Sub-test 4: sell with violated rate limit") + + sellReq2 := &rfqrpc.AddAssetSellOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + PaymentMaxAmt: 42000, + AssetRateLimit: &rfqrpc.FixedPoint{ + Coefficient: "500000", + Scale: 3, + }, + Expiry: expiry, + PeerPubKey: ts.BobLnd.PubKey[:], + TimeoutSeconds: uint32(rfqTimeout.Seconds()), + SkipAssetChannelCheck: true, + } + _, err = ts.AliceTapd.AddAssetSellOrder(ctx, sellReq2) + require.ErrorContains( + t.t, err, "rejected quote", + "expected rate bound rejection for sell order", + ) + + // ----------------------------------------------------------------- + // Sub-test 5: Client validation — min > max rejected locally. + // + // Carol sends a buy order with asset_min_amt = 10 and + // asset_max_amt = 5. The Validate() check in NewBuyRequest + // catches this before any wire negotiation. + // ----------------------------------------------------------------- + t.Log("Sub-test 5: client validation min > max") + + buyReq3 := &rfqrpc.AddAssetBuyOrderRequest{ + AssetSpecifier: &rfqrpc.AssetSpecifier{ + Id: &rfqrpc.AssetSpecifier_AssetId{ + AssetId: mintedAssetId, + }, + }, + AssetMaxAmt: 5, + AssetMinAmt: 10, + Expiry: expiry, + PeerPubKey: ts.BobLnd.PubKey[:], + TimeoutSeconds: uint32(rfqTimeout.Seconds()), + SkipAssetChannelCheck: true, + } + _, err = ts.CarolTapd.AddAssetBuyOrder(ctx, buyReq3) + require.ErrorContains( + t.t, err, "exceeds max amount", + "expected immediate min > max rejection", + ) +} + // rfqTestScenario is a struct which holds test scenario helper infra. type rfqTestScenario struct { testHarness *harnessTest diff --git a/itest/test_list_on_test.go b/itest/test_list_on_test.go index 177701e28..30156adb3 100644 --- a/itest/test_list_on_test.go +++ b/itest/test_list_on_test.go @@ -383,6 +383,10 @@ var allTestCases = []*testCase{ name: "rfq portfolio pilot rpc", test: testRfqPortfolioPilotRpc, }, + { + name: "rfq limit constraints", + test: testRfqLimitConstraints, + }, { name: "multi signature on all levels", test: testMultiSignature, diff --git a/rfq/manager.go b/rfq/manager.go index b424bddf6..48070c86f 100644 --- a/rfq/manager.go +++ b/rfq/manager.go @@ -1440,6 +1440,18 @@ const ( // ValidAcceptQuoteRespStatus indicates that the accepted quote passed // all validation checks successfully. ValidAcceptQuoteRespStatus QuoteRespStatus = 4 + + // MinFillNotMetQuoteRespStatus indicates that the minimum fill + // constraint was not satisfiable at the accepted rate. + MinFillNotMetQuoteRespStatus QuoteRespStatus = 5 + + // RateBoundMissQuoteRespStatus indicates that the accepted rate + // violated the requester's rate limit constraint. + RateBoundMissQuoteRespStatus QuoteRespStatus = 6 + + // FOKNotViableQuoteRespStatus indicates that the FOK execution + // policy could not be satisfied at the accepted rate. + FOKNotViableQuoteRespStatus QuoteRespStatus = 7 ) // InvalidQuoteRespEvent is an event that is broadcast when the RFQ manager diff --git a/rfq/negotiator.go b/rfq/negotiator.go index 569bea1fc..8f7d90f7a 100644 --- a/rfq/negotiator.go +++ b/rfq/negotiator.go @@ -236,7 +236,9 @@ func (n *Negotiator) HandleOutgoingBuyOrder(ctx context.Context, // Construct a new buy request to send to the peer. request, err := rfqmsg.NewBuyRequest( peer, buyOrder.AssetSpecifier, buyOrder.AssetMaxAmt, + buyOrder.AssetMinAmt, buyOrder.AssetRateLimit, assetRateHint, buyOrder.PriceOracleMetadata, + buyOrder.ExecutionPolicy, ) if err != nil { err := fmt.Errorf("unable to create buy request "+ @@ -391,8 +393,10 @@ func (n *Negotiator) HandleOutgoingSellOrder(ctx context.Context, ) request, err := rfqmsg.NewSellRequest( - peer, order.AssetSpecifier, order.PaymentMaxAmt, assetRateHint, - order.PriceOracleMetadata, + peer, order.AssetSpecifier, order.PaymentMaxAmt, + order.PaymentMinAmt, order.AssetRateLimit, + assetRateHint, order.PriceOracleMetadata, + order.ExecutionPolicy, ) if err != nil { diff --git a/rfq/order.go b/rfq/order.go index 68783e806..594523178 100644 --- a/rfq/order.go +++ b/rfq/order.go @@ -191,6 +191,10 @@ type AssetSalePolicy struct { // peer is the peer pub key of the peer we established this policy with. peer route.Vertex + // ExecutionPolicy is the execution policy from the original + // request (annotation only; not enforced at the HTLC level). + ExecutionPolicy fn.Option[rfqmsg.ExecutionPolicy] + // expiry is the policy's expiry unix timestamp after which the policy // is no longer valid. expiry uint64 @@ -212,6 +216,7 @@ func NewAssetSalePolicy(quote rfqmsg.BuyAccept, noop bool, NoOpHTLCs: noop, auxChanNegotiator: chanNegotiator, peer: quote.Peer, + ExecutionPolicy: quote.Request.ExecutionPolicy, } } @@ -426,6 +431,10 @@ type AssetPurchasePolicy struct { // that they carry. htlcToAmt map[models.CircuitKey]lnwire.MilliSatoshi + // ExecutionPolicy is the execution policy from the original + // request (annotation only; not enforced at the HTLC level). + ExecutionPolicy fn.Option[rfqmsg.ExecutionPolicy] + // expiry is the policy's expiry unix timestamp in seconds after which // the policy is no longer valid. expiry uint64 @@ -443,6 +452,7 @@ func NewAssetPurchasePolicy(quote rfqmsg.SellAccept) *AssetPurchasePolicy { PaymentMaxAmt: quote.Request.PaymentMaxAmt, expiry: uint64(quote.AssetRate.Expiry.Unix()), htlcToAmt: htlcToAmtMap, + ExecutionPolicy: quote.Request.ExecutionPolicy, } } @@ -1733,6 +1743,17 @@ type BuyOrder struct { // be willing to offer. AssetMaxAmt uint64 + // AssetMinAmt is an optional minimum asset amount for the order. + AssetMinAmt fn.Option[uint64] + + // AssetRateLimit is an optional minimum acceptable rate (asset + // units per BTC) for the order. + AssetRateLimit fn.Option[rfqmath.BigIntFixedPoint] + + // ExecutionPolicy is an optional execution policy (IOC or + // FOK) for the order. + ExecutionPolicy fn.Option[rfqmsg.ExecutionPolicy] + // Expiry is the time at which the order expires. Expiry time.Time @@ -1800,6 +1821,17 @@ type SellOrder struct { // must agree to pay. PaymentMaxAmt lnwire.MilliSatoshi + // PaymentMinAmt is an optional minimum msat amount for the order. + PaymentMinAmt fn.Option[lnwire.MilliSatoshi] + + // AssetRateLimit is an optional maximum acceptable rate (asset + // units per BTC) for the order. + AssetRateLimit fn.Option[rfqmath.BigIntFixedPoint] + + // ExecutionPolicy is an optional execution policy (IOC or + // FOK) for the order. + ExecutionPolicy fn.Option[rfqmsg.ExecutionPolicy] + // Expiry is the time at which the order expires. Expiry time.Time diff --git a/rfq/portfolio_pilot.go b/rfq/portfolio_pilot.go index 8a690593a..f1f4c53ac 100644 --- a/rfq/portfolio_pilot.go +++ b/rfq/portfolio_pilot.go @@ -363,6 +363,24 @@ func (p *InternalPortfolioPilot) VerifyAcceptQuote(ctx context.Context, return InvalidAssetRatesQuoteRespStatus, nil } + // Enforce the requester's rate bound constraint if set. + status := checkRateBound(req, counterRate.Rate) + if status != ValidAcceptQuoteRespStatus { + return status, nil + } + + // Enforce the requester's min fill constraint if set. + status = checkMinFill(req, counterRate.Rate) + if status != ValidAcceptQuoteRespStatus { + return status, nil + } + + // Enforce FOK execution policy if set. + status = checkFOK(req, counterRate.Rate) + if status != ValidAcceptQuoteRespStatus { + return status, nil + } + return ValidAcceptQuoteRespStatus, nil } @@ -443,3 +461,139 @@ func (p *InternalPortfolioPilot) QueryAssetRates(ctx context.Context, func (p *InternalPortfolioPilot) Close() error { return nil } + +// checkRateBound verifies that the accepted rate satisfies the +// requester's rate limit constraint. For a buy request, the accepted +// rate must be >= the limit (buyer's floor). For a sell request, the +// accepted rate must be <= the limit (seller's ceiling). +func checkRateBound(req rfqmsg.Request, + acceptedRate rfqmath.BigIntFixedPoint) QuoteRespStatus { + + switch r := req.(type) { + case *rfqmsg.BuyRequest: + miss := fn.MapOptionZ( + r.AssetRateLimit, + func(limit rfqmath.BigIntFixedPoint) bool { + return acceptedRate.Cmp(limit) < 0 + }, + ) + if miss { + return RateBoundMissQuoteRespStatus + } + + case *rfqmsg.SellRequest: + miss := fn.MapOptionZ( + r.AssetRateLimit, + func(limit rfqmath.BigIntFixedPoint) bool { + return acceptedRate.Cmp(limit) > 0 + }, + ) + if miss { + return RateBoundMissQuoteRespStatus + } + + default: + log.Warnf("checkRateBound: unhandled request type %T", + req) + } + + return ValidAcceptQuoteRespStatus +} + +// isFOK returns true if the execution policy is Fill-Or-Kill. +func isFOK(p fn.Option[rfqmsg.ExecutionPolicy]) bool { + return fn.MapOptionZ(p, func(ep rfqmsg.ExecutionPolicy) bool { + return ep == rfqmsg.ExecutionPolicyFOK + }) +} + +// checkFOK verifies that the full max amount is transportable at the +// accepted rate when the execution policy is FOK. For a buy request, +// the max asset amount must convert to non-zero msat. For a sell +// request, the max payment amount must convert to non-zero asset units. +func checkFOK(req rfqmsg.Request, + acceptedRate rfqmath.BigIntFixedPoint) QuoteRespStatus { + + switch r := req.(type) { + case *rfqmsg.BuyRequest: + if !isFOK(r.ExecutionPolicy) { + return ValidAcceptQuoteRespStatus + } + + units := rfqmath.NewBigIntFixedPoint( + r.AssetMaxAmt, 0, + ) + msat := rfqmath.UnitsToMilliSatoshi( + units, acceptedRate, + ) + if msat == 0 { + return FOKNotViableQuoteRespStatus + } + + case *rfqmsg.SellRequest: + if !isFOK(r.ExecutionPolicy) { + return ValidAcceptQuoteRespStatus + } + + units := rfqmath.MilliSatoshiToUnits( + r.PaymentMaxAmt, acceptedRate, + ) + zero := rfqmath.NewBigIntFromUint64(0) + if units.Coefficient.Equals(zero) { + return FOKNotViableQuoteRespStatus + } + + default: + log.Warnf("checkFOK: unhandled request type %T", req) + } + + return ValidAcceptQuoteRespStatus +} + +// checkMinFill verifies that the requester's minimum fill amount is +// transportable at the accepted rate. For a buy request, the min asset +// amount must convert to a non-zero msat value. For a sell request, +// the min payment amount must convert to non-zero asset units. +func checkMinFill(req rfqmsg.Request, + acceptedRate rfqmath.BigIntFixedPoint) QuoteRespStatus { + + switch r := req.(type) { + case *rfqmsg.BuyRequest: + notMet := fn.MapOptionZ( + r.AssetMinAmt, + func(minAmt uint64) bool { + units := rfqmath.NewBigIntFixedPoint( + minAmt, 0, + ) + msat := rfqmath.UnitsToMilliSatoshi( + units, acceptedRate, + ) + return msat == 0 + }, + ) + if notMet { + return MinFillNotMetQuoteRespStatus + } + + case *rfqmsg.SellRequest: + notMet := fn.MapOptionZ( + r.PaymentMinAmt, + func(minAmt lnwire.MilliSatoshi) bool { + units := rfqmath.MilliSatoshiToUnits( + minAmt, acceptedRate, + ) + zero := rfqmath.NewBigIntFromUint64(0) + return units.Coefficient.Equals(zero) + }, + ) + if notMet { + return MinFillNotMetQuoteRespStatus + } + + default: + log.Warnf("checkMinFill: unhandled request type %T", + req) + } + + return ValidAcceptQuoteRespStatus +} diff --git a/rfq/portfolio_pilot_rpc.go b/rfq/portfolio_pilot_rpc.go index e0bdc965e..5d13968ef 100644 --- a/rfq/portfolio_pilot_rpc.go +++ b/rfq/portfolio_pilot_rpc.go @@ -8,9 +8,11 @@ import ( "time" "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightninglabs/taproot-assets/rpcutils" pilotrpc "github.com/lightninglabs/taproot-assets/taprpc/portfoliopilotrpc" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "google.golang.org/grpc" "google.golang.org/grpc/credentials" @@ -441,14 +443,25 @@ func rpcMarshalBuyRequest( return nil, fmt.Errorf("marshal rate hint: %w", err) } + var rpcRateLimit *pilotrpc.FixedPoint + req.AssetRateLimit.WhenSome( + func(fp rfqmath.BigIntFixedPoint) { + rpcRateLimit = rpcutils.MarshalPortfolioFixedPoint(fp) + }, + ) + peer := req.MsgPeer() - rpcSpecifier := rpcMarshalPortfolioAssetSpecifier(req.AssetSpecifier) + rpcSpecifier := rpcMarshalPortfolioAssetSpecifier( + req.AssetSpecifier, + ) return &pilotrpc.BuyRequest{ AssetSpecifier: rpcSpecifier, AssetMaxAmount: req.AssetMaxAmt, AssetRateHint: rpcRateHint, PriceOracleMetadata: req.PriceOracleMetadata, PeerId: peer[:], + AssetMinAmount: req.AssetMinAmt.UnwrapOr(0), + AssetRateLimit: rpcRateLimit, }, nil } @@ -475,14 +488,32 @@ func rpcMarshalSellRequest( return nil, fmt.Errorf("marshal rate hint: %w", err) } + var rpcRateLimit *pilotrpc.FixedPoint + req.AssetRateLimit.WhenSome( + func(fp rfqmath.BigIntFixedPoint) { + rpcRateLimit = rpcutils.MarshalPortfolioFixedPoint(fp) + }, + ) + + var paymentMinAmt uint64 + req.PaymentMinAmt.WhenSome( + func(v lnwire.MilliSatoshi) { + paymentMinAmt = uint64(v) + }, + ) + peer := req.MsgPeer() - rpcSpecifier := rpcMarshalPortfolioAssetSpecifier(req.AssetSpecifier) + rpcSpecifier := rpcMarshalPortfolioAssetSpecifier( + req.AssetSpecifier, + ) return &pilotrpc.SellRequest{ AssetSpecifier: rpcSpecifier, PaymentMaxAmount: uint64(req.PaymentMaxAmt), AssetRateHint: rpcRateHint, PriceOracleMetadata: req.PriceOracleMetadata, PeerId: peer[:], + PaymentMinAmount: paymentMinAmt, + AssetRateLimit: rpcRateLimit, }, nil } @@ -573,6 +604,12 @@ func rpcUnmarshalQuoteRespStatus( return PortfolioPilotErrQuoteRespStatus, nil case pilotrpc.QuoteRespStatus_VALID_ACCEPT_QUOTE: return ValidAcceptQuoteRespStatus, nil + case pilotrpc.QuoteRespStatus_MIN_FILL_NOT_MET: + return MinFillNotMetQuoteRespStatus, nil + case pilotrpc.QuoteRespStatus_RATE_BOUND_MISS: + return RateBoundMissQuoteRespStatus, nil + case pilotrpc.QuoteRespStatus_FOK_NOT_VIABLE: + return FOKNotViableQuoteRespStatus, nil default: return 0, fmt.Errorf("unknown quote response status: %v", status) @@ -588,6 +625,10 @@ func rpcUnmarshalRejectCode( return rfqmsg.PriceOracleUnspecifiedRejectCode case pilotrpc.RejectCode_REJECT_CODE_PRICE_ORACLE_UNAVAILABLE: return rfqmsg.PriceOracleUnavailableRejectCode + case pilotrpc.RejectCode_REJECT_CODE_MIN_FILL_NOT_MET: + return rfqmsg.MinFillNotMetRejectCode + case pilotrpc.RejectCode_REJECT_CODE_PRICE_BOUND_MISS: + return rfqmsg.PriceBoundMissRejectCode default: return rfqmsg.PriceOracleUnspecifiedRejectCode } diff --git a/rfq/portfolio_pilot_test.go b/rfq/portfolio_pilot_test.go index 86774ee46..3b6182857 100644 --- a/rfq/portfolio_pilot_test.go +++ b/rfq/portfolio_pilot_test.go @@ -124,7 +124,10 @@ func TestResolveRequest(t *testing.T) { req, err := rfqmsg.NewBuyRequest( route.Vertex{0x01, 0x02, 0x03}, asset.NewSpecifierFromId(asset.ID{assetID}), 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), rateHint, "order-metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) return req @@ -140,7 +143,11 @@ func TestResolveRequest(t *testing.T) { req, err := rfqmsg.NewSellRequest( route.Vertex{0x0A, 0x0B, 0x0C}, asset.NewSpecifierFromId(asset.ID{assetID}), - paymentMax, rateHint, "order-metadata", + paymentMax, + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), + rateHint, "order-metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) return req @@ -546,8 +553,11 @@ func TestVerifyAcceptQuote(t *testing.T) { makeAccept: func(t *testing.T) rfqmsg.Accept { buyReq, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -574,8 +584,11 @@ func TestVerifyAcceptQuote(t *testing.T) { makeAccept: func(t *testing.T) rfqmsg.Accept { buyReq, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -599,8 +612,11 @@ func TestVerifyAcceptQuote(t *testing.T) { makeAccept: func(t *testing.T) rfqmsg.Accept { buyReq, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -629,8 +645,11 @@ func TestVerifyAcceptQuote(t *testing.T) { makeAccept: func(t *testing.T) rfqmsg.Accept { buyReq, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -655,8 +674,11 @@ func TestVerifyAcceptQuote(t *testing.T) { makeAccept: func(t *testing.T) rfqmsg.Accept { buyReq, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -680,8 +702,11 @@ func TestVerifyAcceptQuote(t *testing.T) { makeAccept: func(t *testing.T) rfqmsg.Accept { buyReq, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -708,8 +733,11 @@ func TestVerifyAcceptQuote(t *testing.T) { sellReq, err := rfqmsg.NewSellRequest( peerID, assetSpec, lnwire.MilliSatoshi(1000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -737,8 +765,11 @@ func TestVerifyAcceptQuote(t *testing.T) { sellReq, err := rfqmsg.NewSellRequest( peerID, assetSpec, lnwire.MilliSatoshi(1000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -763,8 +794,11 @@ func TestVerifyAcceptQuote(t *testing.T) { sellReq, err := rfqmsg.NewSellRequest( peerID, assetSpec, lnwire.MilliSatoshi(1000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -789,8 +823,11 @@ func TestVerifyAcceptQuote(t *testing.T) { sellReq, err := rfqmsg.NewSellRequest( peerID, assetSpec, lnwire.MilliSatoshi(1000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -811,6 +848,413 @@ func TestVerifyAcceptQuote(t *testing.T) { expectStatus: InvalidAssetRatesQuoteRespStatus, expectErr: false, }, + + // --- Rate bound enforcement cases --- + + { + name: "buy accept: rate below limit", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Buyer sets floor at 150 units/BTC, + // but peer offers only 100. + limit := rfqmath.NewBigIntFixedPoint( + 150, 0, + ) + buyReq, err := rfqmsg.NewBuyRequest( + peerID, assetSpec, 100, + fn.None[uint64](), + fn.Some(limit), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.None[rfqmsg.ExecutionPolicy](), + ) + require.NoError(t, err) + + return &rfqmsg.BuyAccept{ + Peer: peerID, + Request: *buyReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + expectQueryBuyPrice( + p, &OracleResponse{ + AssetRate: oracleRateMatch, + }, nil, + ) + }, + expectStatus: RateBoundMissQuoteRespStatus, + expectErr: false, + }, + { + name: "buy accept: rate at limit", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Buyer floor exactly equals accepted + // rate. + limit := rfqmath.NewBigIntFixedPoint( + 100, 0, + ) + buyReq, err := rfqmsg.NewBuyRequest( + peerID, assetSpec, 100, + fn.None[uint64](), + fn.Some(limit), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.None[rfqmsg.ExecutionPolicy](), + ) + require.NoError(t, err) + + return &rfqmsg.BuyAccept{ + Peer: peerID, + Request: *buyReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + expectQueryBuyPrice( + p, &OracleResponse{ + AssetRate: oracleRateMatch, + }, nil, + ) + }, + expectStatus: ValidAcceptQuoteRespStatus, + expectErr: false, + }, + { + name: "buy accept: rate above limit", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Buyer floor is 50, accepted rate is + // 100 — should pass. + limit := rfqmath.NewBigIntFixedPoint( + 50, 0, + ) + buyReq, err := rfqmsg.NewBuyRequest( + peerID, assetSpec, 100, + fn.None[uint64](), + fn.Some(limit), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.None[rfqmsg.ExecutionPolicy](), + ) + require.NoError(t, err) + + return &rfqmsg.BuyAccept{ + Peer: peerID, + Request: *buyReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + expectQueryBuyPrice( + p, &OracleResponse{ + AssetRate: oracleRateMatch, + }, nil, + ) + }, + expectStatus: ValidAcceptQuoteRespStatus, + expectErr: false, + }, + { + name: "sell accept: rate above limit", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Seller ceiling is 50, but accepted + // rate is 100 — should fail. + limit := rfqmath.NewBigIntFixedPoint( + 50, 0, + ) + sellReq, err := rfqmsg.NewSellRequest( + peerID, assetSpec, + lnwire.MilliSatoshi(1000), + fn.None[lnwire.MilliSatoshi](), + fn.Some(limit), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.None[rfqmsg.ExecutionPolicy](), + ) + require.NoError(t, err) + + return &rfqmsg.SellAccept{ + Peer: peerID, + Request: *sellReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + resp := OracleResponse{ + AssetRate: oracleRateMatch, + } + expectQuerySellPrice(p, &resp, nil) + }, + expectStatus: RateBoundMissQuoteRespStatus, + expectErr: false, + }, + { + name: "sell accept: rate at limit", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Seller ceiling equals accepted rate. + limit := rfqmath.NewBigIntFixedPoint( + 100, 0, + ) + sellReq, err := rfqmsg.NewSellRequest( + peerID, assetSpec, + lnwire.MilliSatoshi(1000), + fn.None[lnwire.MilliSatoshi](), + fn.Some(limit), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.None[rfqmsg.ExecutionPolicy](), + ) + require.NoError(t, err) + + return &rfqmsg.SellAccept{ + Peer: peerID, + Request: *sellReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + resp := OracleResponse{ + AssetRate: oracleRateMatch, + } + expectQuerySellPrice(p, &resp, nil) + }, + expectStatus: ValidAcceptQuoteRespStatus, + expectErr: false, + }, + + // --- Min fill enforcement cases --- + + { + name: "buy accept: min fill zero msat", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Rate is very high (1e12 units/BTC), + // so 1 unit converts to ~0 msat. + hugeRate := rfqmsg.NewAssetRate( + rfqmath.NewBigIntFixedPoint( + 1_000_000_000_000, 0, + ), + validExpiryFuture, + ) + buyReq, err := rfqmsg.NewBuyRequest( + peerID, assetSpec, 100, + fn.Some[uint64](1), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.None[rfqmsg.ExecutionPolicy](), + ) + require.NoError(t, err) + + return &rfqmsg.BuyAccept{ + Peer: peerID, + Request: *buyReq, + AssetRate: hugeRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + oracleRate := rfqmsg.NewAssetRate( + rfqmath.NewBigIntFixedPoint( + 1_000_000_000_000, 0, + ), + validExpiryFuture, + ) + expectQueryBuyPrice( + p, &OracleResponse{ + AssetRate: oracleRate, + }, nil, + ) + }, + expectStatus: MinFillNotMetQuoteRespStatus, + expectErr: false, + }, + { + name: "buy accept: min fill transportable", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Rate of 100 units/BTC, min of 50 + // units = 0.5 BTC = 500M msat. Easily + // transportable. + buyReq, err := rfqmsg.NewBuyRequest( + peerID, assetSpec, 100, + fn.Some[uint64](50), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.None[rfqmsg.ExecutionPolicy](), + ) + require.NoError(t, err) + + return &rfqmsg.BuyAccept{ + Peer: peerID, + Request: *buyReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + expectQueryBuyPrice( + p, &OracleResponse{ + AssetRate: oracleRateMatch, + }, nil, + ) + }, + expectStatus: ValidAcceptQuoteRespStatus, + expectErr: false, + }, + + // --- FOK enforcement cases --- + + { + name: "buy accept: FOK viable", + makeAccept: func(t *testing.T) rfqmsg.Accept { + buyReq, err := rfqmsg.NewBuyRequest( + peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + ) + require.NoError(t, err) + + return &rfqmsg.BuyAccept{ + Peer: peerID, + Request: *buyReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + expectQueryBuyPrice( + p, &OracleResponse{ + AssetRate: oracleRateMatch, + }, nil, + ) + }, + expectStatus: ValidAcceptQuoteRespStatus, + expectErr: false, + }, + { + name: "buy accept: FOK not viable", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Rate is huge: 1 unit converts + // to ~0 msat. + hugeRate := rfqmsg.NewAssetRate( + rfqmath.NewBigIntFixedPoint( + 1_000_000_000_000, 0, + ), + validExpiryFuture, + ) + buyReq, err := rfqmsg.NewBuyRequest( + peerID, assetSpec, 1, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + ) + require.NoError(t, err) + + return &rfqmsg.BuyAccept{ + Peer: peerID, + Request: *buyReq, + AssetRate: hugeRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + oracleRate := rfqmsg.NewAssetRate( + rfqmath.NewBigIntFixedPoint( + 1_000_000_000_000, 0, + ), + validExpiryFuture, + ) + expectQueryBuyPrice( + p, &OracleResponse{ + AssetRate: oracleRate, + }, nil, + ) + }, + expectStatus: FOKNotViableQuoteRespStatus, + expectErr: false, + }, + { + name: "sell accept: FOK viable", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // 50B msat at 100 units/BTC = + // 5 units (non-zero). + sellReq, err := rfqmsg.NewSellRequest( + peerID, assetSpec, + lnwire.MilliSatoshi(50_000_000_000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + ) + require.NoError(t, err) + + return &rfqmsg.SellAccept{ + Peer: peerID, + Request: *sellReq, + AssetRate: peerRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + resp := OracleResponse{ + AssetRate: oracleRateMatch, + } + expectQuerySellPrice(p, &resp, nil) + }, + expectStatus: ValidAcceptQuoteRespStatus, + expectErr: false, + }, + { + name: "sell accept: FOK not viable", + makeAccept: func(t *testing.T) rfqmsg.Accept { + // Low rate (1 unit/BTC): 1 msat + // converts to 0 units. + lowRate := rfqmsg.NewAssetRate( + rfqmath.NewBigIntFixedPoint( + 1, 0, + ), + validExpiryFuture, + ) + sellReq, err := rfqmsg.NewSellRequest( + peerID, assetSpec, + lnwire.MilliSatoshi(1), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[rfqmsg.AssetRate](), + "metadata", + fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + ) + require.NoError(t, err) + + return &rfqmsg.SellAccept{ + Peer: peerID, + Request: *sellReq, + AssetRate: lowRate, + } + }, + setupOracle: func(p *MockPriceOracle) { + oracleRate := rfqmsg.NewAssetRate( + rfqmath.NewBigIntFixedPoint( + 1, 0, + ), + validExpiryFuture, + ) + resp := OracleResponse{ + AssetRate: oracleRate, + } + expectQuerySellPrice(p, &resp, nil) + }, + expectStatus: FOKNotViableQuoteRespStatus, + expectErr: false, + }, } for _, tc := range tests { @@ -867,8 +1311,11 @@ func TestResolveRequestWithoutPriceOracleRejects(t *testing.T) { req, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -906,8 +1353,11 @@ func TestVerifyAcceptQuoteWithoutPriceOracle(t *testing.T) { buyReq, err := rfqmsg.NewBuyRequest( peerID, assetSpec, 100, + fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), fn.None[rfqmsg.AssetRate](), "metadata", + fn.None[rfqmsg.ExecutionPolicy](), ) require.NoError(t, err) @@ -932,3 +1382,320 @@ func TestVerifyAcceptQuoteWithoutPriceOracle(t *testing.T) { require.NoError(t, err) require.Equal(t, PriceOracleQueryErrQuoteRespStatus, status) } + +// TestCheckRateBound exercises the checkRateBound helper directly. +func TestCheckRateBound(t *testing.T) { + t.Parallel() + + spec := asset.NewSpecifierFromId(asset.ID{0x01}) + rate100 := rfqmath.NewBigIntFixedPoint(100, 0) + rate50 := rfqmath.NewBigIntFixedPoint(50, 0) + rate150 := rfqmath.NewBigIntFixedPoint(150, 0) + noLimit := fn.None[rfqmath.BigIntFixedPoint]() + + tests := []struct { + name string + req rfqmsg.Request + rate rfqmath.BigIntFixedPoint + expect QuoteRespStatus + }{ + { + name: "buy: no limit set", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetRateLimit: noLimit, + }, + rate: rate100, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: rate above limit", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetRateLimit: fn.Some(rate50), + }, + rate: rate100, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: rate at limit", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetRateLimit: fn.Some(rate100), + }, + rate: rate100, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: rate below limit", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetRateLimit: fn.Some(rate150), + }, + rate: rate100, + expect: RateBoundMissQuoteRespStatus, + }, + { + name: "sell: no limit set", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + AssetRateLimit: noLimit, + }, + rate: rate100, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "sell: rate below limit", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + AssetRateLimit: fn.Some(rate150), + }, + rate: rate100, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "sell: rate at limit", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + AssetRateLimit: fn.Some(rate100), + }, + rate: rate100, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "sell: rate above limit", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + AssetRateLimit: fn.Some(rate50), + }, + rate: rate100, + expect: RateBoundMissQuoteRespStatus, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + status := checkRateBound(tc.req, tc.rate) + require.Equal(t, tc.expect, status) + }) + } +} + +// TestCheckMinFill exercises the checkMinFill helper directly. +func TestCheckMinFill(t *testing.T) { + t.Parallel() + + spec := asset.NewSpecifierFromId(asset.ID{0x01}) + + // A rate of 100 units/BTC. 50 units = 0.5 BTC = + // 50_000_000_000 msat (easily non-zero). + normalRate := rfqmath.NewBigIntFixedPoint(100, 0) + + // A huge rate: 1e12 units/BTC. 1 unit = 1e-12 BTC = + // 0.1 msat, rounds to 0. + hugeRate := rfqmath.NewBigIntFixedPoint( + 1_000_000_000_000, 0, + ) + + tests := []struct { + name string + req rfqmsg.Request + rate rfqmath.BigIntFixedPoint + expect QuoteRespStatus + }{ + { + name: "buy: no min set", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetMinAmt: fn.None[uint64](), + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: min fill transportable", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetMinAmt: fn.Some[uint64](50), + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: min fill rounds to zero", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetMinAmt: fn.Some[uint64](1), + }, + rate: hugeRate, + expect: MinFillNotMetQuoteRespStatus, + }, + { + name: "sell: no min set", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + PaymentMinAmt: fn.None[lnwire.MilliSatoshi](), + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "sell: min fill transportable", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: lnwire.MilliSatoshi( + 50_000_000_000, + ), + PaymentMinAmt: fn.Some( + lnwire.MilliSatoshi( + 10_000_000_000, + ), + ), + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + // NOTE: A sell min fill that rounds to zero units is + // very hard to trigger with the default arithmetic + // scale (11), since MilliSatoshiToUnits preserves + // enough precision for any non-zero msat. The + // check acts as a safety net for degenerate inputs. + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + status := checkMinFill(tc.req, tc.rate) + require.Equal(t, tc.expect, status) + }) + } +} + +// TestCheckFOK exercises the checkFOK helper directly. +func TestCheckFOK(t *testing.T) { + t.Parallel() + + spec := asset.NewSpecifierFromId(asset.ID{0x01}) + + // Normal rate: 100 units/BTC. + normalRate := rfqmath.NewBigIntFixedPoint(100, 0) + + // Huge rate: 1e12 units/BTC. Max of 1 unit converts + // to ~0 msat. + hugeRate := rfqmath.NewBigIntFixedPoint( + 1_000_000_000_000, 0, + ) + + tests := []struct { + name string + req rfqmsg.Request + rate rfqmath.BigIntFixedPoint + expect QuoteRespStatus + }{ + { + name: "buy: no policy", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: IOC policy", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + ExecutionPolicy: fn.Some( + rfqmsg.ExecutionPolicyIOC, + ), + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: FOK viable", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 100, + ExecutionPolicy: fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "buy: FOK rounds to zero", + req: &rfqmsg.BuyRequest{ + AssetSpecifier: spec, + AssetMaxAmt: 1, + ExecutionPolicy: fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + }, + rate: hugeRate, + expect: FOKNotViableQuoteRespStatus, + }, + { + name: "sell: no policy", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "sell: FOK viable", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: lnwire.MilliSatoshi( + 50_000_000_000, + ), + ExecutionPolicy: fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + }, + rate: normalRate, + expect: ValidAcceptQuoteRespStatus, + }, + { + name: "sell: FOK rounds to zero", + req: &rfqmsg.SellRequest{ + AssetSpecifier: spec, + PaymentMaxAmt: lnwire.MilliSatoshi(1), + ExecutionPolicy: fn.Some( + rfqmsg.ExecutionPolicyFOK, + ), + }, + // 1 unit/BTC: 1 msat = 1e-11 BTC * 1 = 0 units. + rate: rfqmath.NewBigIntFixedPoint(1, 0), + expect: FOKNotViableQuoteRespStatus, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + status := checkFOK(tc.req, tc.rate) + require.Equal(t, tc.expect, status) + }) + } +} diff --git a/rfqmsg/buy_request.go b/rfqmsg/buy_request.go index b61ed89a7..2f9ced8db 100644 --- a/rfqmsg/buy_request.go +++ b/rfqmsg/buy_request.go @@ -8,6 +8,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" ) @@ -54,6 +55,21 @@ type BuyRequest struct { // peer must agree to divest. AssetMaxAmt uint64 + // AssetMinAmt is an optional minimum asset amount for the quote. + // When set, the responding peer must be willing to divest at + // least this many units. + AssetMinAmt fn.Option[uint64] + + // AssetRateLimit is an optional minimum acceptable rate (asset + // units per BTC). The buyer sets a floor: "I won't accept fewer + // than X units per BTC." + AssetRateLimit fn.Option[rfqmath.BigIntFixedPoint] + + // ExecutionPolicy is an optional execution policy for the + // quote request. IOC (default) accepts partial fills; FOK + // requires the full max amount to be viable. + ExecutionPolicy fn.Option[ExecutionPolicy] + // AssetRateHint represents a proposed conversion rate between the // subject asset and BTC. This rate is an initial suggestion intended to // initiate the RFQ negotiation process and may differ from the final @@ -71,8 +87,11 @@ type BuyRequest struct { // NewBuyRequest creates a new asset buy quote request. func NewBuyRequest(peer route.Vertex, assetSpecifier asset.Specifier, - assetMaxAmt uint64, assetRateHint fn.Option[AssetRate], - oracleMetadata string) (*BuyRequest, error) { + assetMaxAmt uint64, assetMinAmt fn.Option[uint64], + assetRateLimit fn.Option[rfqmath.BigIntFixedPoint], + assetRateHint fn.Option[AssetRate], + oracleMetadata string, + execPolicy fn.Option[ExecutionPolicy]) (*BuyRequest, error) { id, err := NewID() if err != nil { @@ -86,15 +105,25 @@ func NewBuyRequest(peer route.Vertex, assetSpecifier asset.Specifier, "length of %d bytes", MaxOracleMetadataLength) } - return &BuyRequest{ + req := &BuyRequest{ Peer: peer, Version: latestBuyRequestVersion, ID: id, AssetSpecifier: assetSpecifier, AssetMaxAmt: assetMaxAmt, + AssetMinAmt: assetMinAmt, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, AssetRateHint: assetRateHint, PriceOracleMetadata: oracleMetadata, - }, nil + } + + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("unable to validate buy "+ + "request: %w", err) + } + + return req, nil } // NewBuyRequestFromWire instantiates a new instance from a wire message. @@ -153,13 +182,43 @@ func NewBuyRequestFromWire(wireMsg WireMessage, }, ) + // Extract optional min asset amount. + var assetMinAmt fn.Option[uint64] + msgData.MinInAsset.WhenSome( + func(r tlv.RecordT[tlv.TlvType23, uint64]) { + if r.Val > 0 { + assetMinAmt = fn.Some(r.Val) + } + }, + ) + + // Extract optional rate limit. + var assetRateLimit fn.Option[rfqmath.BigIntFixedPoint] + msgData.AssetRateLimit.WhenSome( + func(r tlv.RecordT[tlv.TlvType29, TlvFixedPoint]) { + fp := r.Val.IntoBigIntFixedPoint() + assetRateLimit = fn.Some(fp) + }, + ) + + // Extract optional execution policy. + var execPolicy fn.Option[ExecutionPolicy] + msgData.ExecutionPolicy.WhenSome( + func(r tlv.RecordT[tlv.TlvType31, uint8]) { + execPolicy = fn.Some(ExecutionPolicy(r.Val)) + }, + ) + req := BuyRequest{ - Peer: wireMsg.Peer, - Version: msgData.Version.Val, - ID: msgData.ID.Val, - AssetSpecifier: assetSpecifier, - AssetMaxAmt: msgData.MaxInAsset.Val, - AssetRateHint: assetRateHint, + Peer: wireMsg.Peer, + Version: msgData.Version.Val, + ID: msgData.ID.Val, + AssetSpecifier: assetSpecifier, + AssetMaxAmt: msgData.MaxInAsset.Val, + AssetMinAmt: assetMinAmt, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, + AssetRateHint: assetRateHint, } msgData.PriceOracleMetadata.ValOpt().WhenSome(func(metaBytes []byte) { @@ -195,6 +254,49 @@ func (q *BuyRequest) Validate() error { "length of %d bytes", MaxOracleMetadataLength) } + // Ensure min <= max when min is set. + err = fn.MapOptionZ(q.AssetMinAmt, func(minAmt uint64) error { + if minAmt > q.AssetMaxAmt { + return fmt.Errorf("asset min amount (%d) exceeds "+ + "max amount (%d)", minAmt, q.AssetMaxAmt) + } + return nil + }) + if err != nil { + return err + } + + // Ensure rate limit is strictly positive when set. + err = fn.MapOptionZ( + q.AssetRateLimit, + func(limit rfqmath.BigIntFixedPoint) error { + zero := rfqmath.NewBigIntFromUint64(0) + if !limit.Coefficient.Gt(zero) { + return fmt.Errorf("asset rate limit " + + "coefficient must be positive") + } + return nil + }, + ) + if err != nil { + return err + } + + // Ensure execution policy is valid when set. + err = fn.MapOptionZ( + q.ExecutionPolicy, + func(p ExecutionPolicy) error { + if p > ExecutionPolicyFOK { + return fmt.Errorf("invalid execution "+ + "policy: %d", p) + } + return nil + }, + ) + if err != nil { + return err + } + // Ensure that the suggested asset rate has not expired. err = fn.MapOptionZ(q.AssetRateHint, func(rate AssetRate) error { if rate.Expiry.Before(time.Now()) { @@ -261,10 +363,29 @@ func (q *BuyRequest) String() string { }, ) + minAmtStr := fn.MapOptionZ(q.AssetMinAmt, func(v uint64) string { + return fmt.Sprintf(", min_asset_amount=%d", v) + }) + + rateLimitStr := fn.MapOptionZ( + q.AssetRateLimit, + func(v rfqmath.BigIntFixedPoint) string { + return fmt.Sprintf(", asset_rate_limit=%s", + v.String()) + }, + ) + + execPolicyStr := fn.MapOptionZ( + q.ExecutionPolicy, + func(p ExecutionPolicy) string { + return fmt.Sprintf(", exec_policy=%d", p) + }, + ) + return fmt.Sprintf("BuyRequest(peer=%x, id=%x, asset=%s, "+ - "max_asset_amount=%d, asset_rate_hint=%s)", + "max_asset_amount=%d%s%s%s, asset_rate_hint=%s)", q.Peer[:], q.ID[:], q.AssetSpecifier.String(), q.AssetMaxAmt, - assetRateHintStr) + minAmtStr, rateLimitStr, execPolicyStr, assetRateHintStr) } // Ensure that the message type implements the OutgoingMsg interface. diff --git a/rfqmsg/buy_request_test.go b/rfqmsg/buy_request_test.go new file mode 100644 index 000000000..5f3b9cc5b --- /dev/null +++ b/rfqmsg/buy_request_test.go @@ -0,0 +1,267 @@ +package rfqmsg + +import ( + "bytes" + "testing" + "time" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// buyRequestRoundtrip encodes a BuyRequest to wire and decodes it +// back, returning the decoded request. +func buyRequestRoundtrip(t *testing.T, + req *BuyRequest) *BuyRequest { + + t.Helper() + + wireMsg, err := req.ToWire() + require.NoError(t, err) + + var msgData requestWireMsgData + err = msgData.Decode(bytes.NewReader(wireMsg.Data)) + require.NoError(t, err) + + decoded, err := NewBuyRequestFromWire(wireMsg, msgData) + require.NoError(t, err) + + return decoded +} + +// TestBuyRequestMinAmtRoundtrip verifies that AssetMinAmt survives +// a wire roundtrip. +func TestBuyRequestMinAmtRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x01} + spec := asset.NewSpecifierFromId(asset.ID{0xAA}) + + req, err := NewBuyRequest( + peer, spec, 100, fn.Some[uint64](50), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := buyRequestRoundtrip(t, req) + + require.True(t, decoded.AssetMinAmt.IsSome()) + decoded.AssetMinAmt.WhenSome(func(v uint64) { + require.Equal(t, uint64(50), v) + }) + require.Equal(t, uint64(100), decoded.AssetMaxAmt) +} + +// TestBuyRequestRateLimitRoundtrip verifies that AssetRateLimit +// survives a wire roundtrip. +func TestBuyRequestRateLimitRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x02} + spec := asset.NewSpecifierFromId(asset.ID{0xBB}) + limit := rfqmath.NewBigIntFixedPoint(42000, 2) + + req, err := NewBuyRequest( + peer, spec, 200, fn.None[uint64](), + fn.Some(limit), fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := buyRequestRoundtrip(t, req) + + require.True(t, decoded.AssetRateLimit.IsSome()) + decoded.AssetRateLimit.WhenSome( + func(v rfqmath.BigIntFixedPoint) { + require.Equal(t, 0, v.Cmp(limit)) + }, + ) +} + +// TestBuyRequestNoOptionalFieldsRoundtrip verifies backward +// compatibility when no optional fields are set. +func TestBuyRequestNoOptionalFieldsRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x03} + spec := asset.NewSpecifierFromId(asset.ID{0xCC}) + + req, err := NewBuyRequest( + peer, spec, 300, fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := buyRequestRoundtrip(t, req) + + require.True(t, decoded.AssetMinAmt.IsNone()) + require.True(t, decoded.AssetRateLimit.IsNone()) + require.Equal(t, uint64(300), decoded.AssetMaxAmt) +} + +// TestBuyRequestAllFieldsRoundtrip verifies that all optional fields +// survive a roundtrip together. +func TestBuyRequestAllFieldsRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x04} + spec := asset.NewSpecifierFromId(asset.ID{0xDD}) + limit := rfqmath.NewBigIntFixedPoint(99000, 3) + expiry := time.Now().Add(5 * time.Minute).UTC() + rateHint := NewAssetRate( + rfqmath.NewBigIntFixedPoint(100000, 0), expiry, + ) + + req, err := NewBuyRequest( + peer, spec, 500, fn.Some[uint64](10), + fn.Some(limit), fn.Some(rateHint), "metadata", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := buyRequestRoundtrip(t, req) + + require.True(t, decoded.AssetMinAmt.IsSome()) + decoded.AssetMinAmt.WhenSome(func(v uint64) { + require.Equal(t, uint64(10), v) + }) + + require.True(t, decoded.AssetRateLimit.IsSome()) + decoded.AssetRateLimit.WhenSome( + func(v rfqmath.BigIntFixedPoint) { + require.Equal(t, 0, v.Cmp(limit)) + }, + ) + + require.True(t, decoded.AssetRateHint.IsSome()) + require.Equal(t, "metadata", decoded.PriceOracleMetadata) +} + +// TestBuyRequestValidateMinGtMax ensures Validate rejects min > max. +func TestBuyRequestValidateMinGtMax(t *testing.T) { + t.Parallel() + + req := &BuyRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + AssetMaxAmt: 100, + AssetMinAmt: fn.Some[uint64](200), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), + AssetRateHint: fn.None[AssetRate](), + } + + err := req.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "exceeds max amount") +} + +// TestBuyRequestValidateMinEqMax ensures min == max is valid. +func TestBuyRequestValidateMinEqMax(t *testing.T) { + t.Parallel() + + req := &BuyRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + AssetMaxAmt: 100, + AssetMinAmt: fn.Some[uint64](100), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), + AssetRateHint: fn.None[AssetRate](), + } + + err := req.Validate() + require.NoError(t, err) +} + +// TestBuyRequestValidateZeroRateLimit ensures a zero rate limit +// coefficient is rejected. +func TestBuyRequestValidateZeroRateLimit(t *testing.T) { + t.Parallel() + + zeroLimit := rfqmath.NewBigIntFixedPoint(0, 0) + req := &BuyRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + AssetMaxAmt: 100, + AssetMinAmt: fn.None[uint64](), + AssetRateLimit: fn.Some(zeroLimit), + AssetRateHint: fn.None[AssetRate](), + } + + err := req.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "must be positive") +} + +// TestBuyRequestExecutionPolicyRoundtrip verifies that a FOK +// execution policy survives a wire roundtrip. +func TestBuyRequestExecutionPolicyRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x05} + spec := asset.NewSpecifierFromId(asset.ID{0xEE}) + + req, err := NewBuyRequest( + peer, spec, 400, fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.Some(ExecutionPolicyFOK), + ) + require.NoError(t, err) + + decoded := buyRequestRoundtrip(t, req) + + require.True(t, decoded.ExecutionPolicy.IsSome()) + decoded.ExecutionPolicy.WhenSome( + func(v ExecutionPolicy) { + require.Equal(t, ExecutionPolicyFOK, v) + }, + ) +} + +// TestBuyRequestNoExecutionPolicyRoundtrip verifies that an absent +// execution policy stays None after a wire roundtrip. +func TestBuyRequestNoExecutionPolicyRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x06} + spec := asset.NewSpecifierFromId(asset.ID{0xFF}) + + req, err := NewBuyRequest( + peer, spec, 500, fn.None[uint64](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := buyRequestRoundtrip(t, req) + + require.True(t, decoded.ExecutionPolicy.IsNone()) +} + +// TestBuyRequestInvalidExecutionPolicy ensures Validate rejects +// an unknown execution policy value. +func TestBuyRequestInvalidExecutionPolicy(t *testing.T) { + t.Parallel() + + req := &BuyRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + AssetMaxAmt: 100, + AssetMinAmt: fn.None[uint64](), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), + AssetRateHint: fn.None[AssetRate](), + ExecutionPolicy: fn.Some(ExecutionPolicy(2)), + } + + err := req.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "execution policy") +} diff --git a/rfqmsg/reject.go b/rfqmsg/reject.go index 2988d6f01..8de809bf1 100644 --- a/rfqmsg/reject.go +++ b/rfqmsg/reject.go @@ -89,6 +89,21 @@ const ( // PriceOracleUnavailableRejectCode indicates that a request-for-quote // was rejected as a price oracle was unavailable. PriceOracleUnavailableRejectCode RejectCode = 1 + + // MinFillNotMetRejectCode indicates that the minimum fill + // constraint was not satisfiable at the accepted rate. + MinFillNotMetRejectCode RejectCode = 2 + + // PriceBoundMissRejectCode indicates that the accepted rate + // violated the requester's rate limit constraint. + PriceBoundMissRejectCode RejectCode = 3 + + // FOKNotViableRejectCode indicates that the FOK execution + // policy could not be satisfied at the accepted rate. + // + // NOTE: Currently unused. Reserved for workstream D where the + // responder may reject via wire Reject message. + FOKNotViableRejectCode RejectCode = 4 ) var ( @@ -105,6 +120,30 @@ var ( Code: PriceOracleUnavailableRejectCode, Msg: "price oracle unavailable", } + + // ErrMinFillNotMet is the error for when the minimum fill + // constraint cannot be met at the accepted rate. + ErrMinFillNotMet = RejectErr{ + Code: MinFillNotMetRejectCode, + Msg: "minimum fill not met", + } + + // ErrPriceBoundMiss is the error for when the accepted rate + // violates the requester's rate limit constraint. + ErrPriceBoundMiss = RejectErr{ + Code: PriceBoundMissRejectCode, + Msg: "rate limit constraint violated", + } + + // ErrFOKNotViable is the error for when the FOK execution + // policy cannot be satisfied at the accepted rate. + // + // NOTE: Currently unused. Reserved for workstream D where the + // responder may reject via wire Reject message. + ErrFOKNotViable = RejectErr{ + Code: FOKNotViableRejectCode, + Msg: "FOK not viable at accepted rate", + } ) // NewRejectErr produces the "unknown" error code, but pairs it with a diff --git a/rfqmsg/request.go b/rfqmsg/request.go index 477f0a6e2..42ffefe71 100644 --- a/rfqmsg/request.go +++ b/rfqmsg/request.go @@ -8,7 +8,9 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/rfqmath" lfn "github.com/lightningnetwork/lnd/fn/v2" + "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/tlv" ) @@ -59,6 +61,33 @@ type ( // the optional metadata that can be included in a quote request to // provide additional context to the price oracle. requestOracleMetadata = tlv.OptionalRecordT[tlv.TlvType27, []byte] + + // requestAssetRateLimit is a type alias for a record that + // represents an optional rate limit constraint on the quote + // request. + requestAssetRateLimit = tlv.OptionalRecordT[ + tlv.TlvType29, TlvFixedPoint, + ] + + // requestExecutionPolicy is a type alias for a record that + // represents an optional execution policy on the quote + // request. + requestExecutionPolicy = tlv.OptionalRecordT[ + tlv.TlvType31, uint8, + ] +) + +// ExecutionPolicy specifies how a quote request should be filled. +type ExecutionPolicy uint8 + +const ( + // ExecutionPolicyIOC (Immediate-Or-Cancel) accepts any partial + // fill >= the min threshold. This is the default behaviour. + ExecutionPolicyIOC ExecutionPolicy = 0 + + // ExecutionPolicyFOK (Fill-Or-Kill) requires the accepted rate + // to support the full max amount or the quote is rejected. + ExecutionPolicyFOK ExecutionPolicy = 1 ) // requestWireMsgData is a struct that represents the message data field for @@ -138,6 +167,15 @@ type requestWireMsgData struct { // price oracle can use to give out a more accurate (or discount) asset // rate. The maximum length of this field is 32'768 bytes. PriceOracleMetadata requestOracleMetadata + + // AssetRateLimit is an optional rate limit constraint. For buy + // requests this is the minimum acceptable rate; for sell requests + // this is the maximum acceptable rate. + AssetRateLimit requestAssetRateLimit + + // ExecutionPolicy is an optional execution policy for the + // quote request (IOC or FOK). + ExecutionPolicy requestExecutionPolicy } // newRequestWireMsgDataFromBuy creates a new requestWireMsgData from a buy @@ -201,6 +239,33 @@ func newRequestWireMsgDataFromBuy(q BuyRequest) (requestWireMsgData, error) { ) } + // Set optional min asset amount. + var minInAsset tlv.OptionalRecordT[tlv.TlvType23, uint64] + q.AssetMinAmt.WhenSome(func(minAmt uint64) { + minInAsset = tlv.SomeRecordT[tlv.TlvType23]( + tlv.NewPrimitiveRecord[tlv.TlvType23](minAmt), + ) + }) + + // Set optional rate limit. + var assetRateLimit requestAssetRateLimit + q.AssetRateLimit.WhenSome(func(limit rfqmath.BigIntFixedPoint) { + wireRate := NewTlvFixedPointFromBigInt(limit) + assetRateLimit = tlv.SomeRecordT[tlv.TlvType29]( + tlv.NewRecordT[tlv.TlvType29](wireRate), + ) + }) + + // Set optional execution policy. + var execPolicy requestExecutionPolicy + q.ExecutionPolicy.WhenSome(func(p ExecutionPolicy) { + execPolicy = tlv.SomeRecordT[tlv.TlvType31]( + tlv.NewPrimitiveRecord[tlv.TlvType31]( + uint8(p), + ), + ) + }) + // Encode message data component as TLV bytes. return requestWireMsgData{ Version: version, @@ -213,6 +278,9 @@ func newRequestWireMsgDataFromBuy(q BuyRequest) (requestWireMsgData, error) { OutAssetGroupKey: outAssetGroupKey, MaxInAsset: maxInAsset, InAssetRateHint: inAssetRateHint, + MinInAsset: minInAsset, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, PriceOracleMetadata: oracleMetadata, }, nil } @@ -281,6 +349,35 @@ func newRequestWireMsgDataFromSell(q SellRequest) (requestWireMsgData, error) { ) } + // Set optional min payment amount. + var minOutAsset tlv.OptionalRecordT[tlv.TlvType25, uint64] + q.PaymentMinAmt.WhenSome(func(minAmt lnwire.MilliSatoshi) { + minOutAsset = tlv.SomeRecordT[tlv.TlvType25]( + tlv.NewPrimitiveRecord[tlv.TlvType25]( + uint64(minAmt), + ), + ) + }) + + // Set optional rate limit. + var assetRateLimit requestAssetRateLimit + q.AssetRateLimit.WhenSome(func(limit rfqmath.BigIntFixedPoint) { + wireRate := NewTlvFixedPointFromBigInt(limit) + assetRateLimit = tlv.SomeRecordT[tlv.TlvType29]( + tlv.NewRecordT[tlv.TlvType29](wireRate), + ) + }) + + // Set optional execution policy. + var execPolicy requestExecutionPolicy + q.ExecutionPolicy.WhenSome(func(p ExecutionPolicy) { + execPolicy = tlv.SomeRecordT[tlv.TlvType31]( + tlv.NewPrimitiveRecord[tlv.TlvType31]( + uint8(p), + ), + ) + }) + // Encode message data component as TLV bytes. return requestWireMsgData{ Version: version, @@ -292,6 +389,9 @@ func newRequestWireMsgDataFromSell(q SellRequest) (requestWireMsgData, error) { OutAssetGroupKey: outAssetGroupKey, MaxInAsset: maxInAsset, OutAssetRateHint: outAssetRateHint, + MinOutAsset: minOutAsset, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, PriceOracleMetadata: oracleMetadata, }, nil } @@ -431,6 +531,16 @@ func (m *requestWireMsgData) Encode(w io.Writer) error { records = append(records, r.Record()) }, ) + m.AssetRateLimit.WhenSome( + func(r tlv.RecordT[tlv.TlvType29, TlvFixedPoint]) { + records = append(records, r.Record()) + }, + ) + m.ExecutionPolicy.WhenSome( + func(r tlv.RecordT[tlv.TlvType31, uint8]) { + records = append(records, r.Record()) + }, + ) tlv.SortRecords(records) @@ -459,6 +569,8 @@ func (m *requestWireMsgData) Decode(r io.Reader) error { minOutAsset := m.MinOutAsset.Zero() oracleMetadata := m.PriceOracleMetadata.Zero() + assetRateLimit := m.AssetRateLimit.Zero() + executionPolicy := m.ExecutionPolicy.Zero() // Create a tlv stream with all the fields. tlvStream, err := tlv.NewStream( @@ -482,6 +594,8 @@ func (m *requestWireMsgData) Decode(r io.Reader) error { minOutAsset.Record(), oracleMetadata.Record(), + assetRateLimit.Record(), + executionPolicy.Record(), ) if err != nil { return err @@ -524,6 +638,12 @@ func (m *requestWireMsgData) Decode(r io.Reader) error { if _, ok := tlvMap[oracleMetadata.TlvType()]; ok { m.PriceOracleMetadata = tlv.SomeRecordT(oracleMetadata) } + if _, ok := tlvMap[assetRateLimit.TlvType()]; ok { + m.AssetRateLimit = tlv.SomeRecordT(assetRateLimit) + } + if _, ok := tlvMap[executionPolicy.TlvType()]; ok { + m.ExecutionPolicy = tlv.SomeRecordT(executionPolicy) + } return nil } diff --git a/rfqmsg/request_property_test.go b/rfqmsg/request_property_test.go new file mode 100644 index 000000000..a43b638a6 --- /dev/null +++ b/rfqmsg/request_property_test.go @@ -0,0 +1,647 @@ +package rfqmsg + +import ( + "bytes" + "fmt" + "math/big" + "testing" + "time" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" + "pgregory.net/rapid" +) + +// optionalUint64Gen draws an fn.Option[uint64] that is None half the +// time and Some(v) otherwise, where v is drawn from [0, bound]. +func optionalUint64Gen(bound uint64) *rapid.Generator[fn.Option[uint64]] { + return rapid.Custom(func(t *rapid.T) fn.Option[uint64] { + if rapid.Bool().Draw(t, "present") { + // Start from 1: zero is treated as unset + // on the wire. + v := rapid.Uint64Range(1, bound).Draw( + t, "value", + ) + return fn.Some(v) + } + return fn.None[uint64]() + }) +} + +// optionalMsatGen draws an fn.Option[lnwire.MilliSatoshi] that is +// None half the time and Some(v) otherwise, where v <= bound. +func optionalMsatGen( + bound uint64) *rapid.Generator[fn.Option[lnwire.MilliSatoshi]] { + + return rapid.Custom( + func(t *rapid.T) fn.Option[lnwire.MilliSatoshi] { + if rapid.Bool().Draw(t, "present") { + // Start from 1: zero is treated as + // unset on the wire. + v := rapid.Uint64Range(1, bound).Draw( + t, "value", + ) + return fn.Some( + lnwire.MilliSatoshi(v), + ) + } + return fn.None[lnwire.MilliSatoshi]() + }, + ) +} + +// fixedPointGen draws a BigIntFixedPoint with coefficient in [1,1e12] +// and scale in [0,11]. +func fixedPointGen() *rapid.Generator[rfqmath.BigIntFixedPoint] { + return rapid.Custom( + func(t *rapid.T) rfqmath.BigIntFixedPoint { + coeff := rapid.Uint64Range(1, 1_000_000_000_000). + Draw(t, "coeff") + scale := rapid.Uint8Range(0, 11).Draw(t, "scale") + return rfqmath.NewBigIntFixedPoint( + coeff, scale, + ) + }, + ) +} + +// optFP is a short alias used in test generators to stay +// within the 80-character line limit. +type optFP = fn.Option[rfqmath.BigIntFixedPoint] + +// optionalFixedPointGen draws an optional BigIntFixedPoint, +// None half the time. +func optionalFixedPointGen() *rapid.Generator[optFP] { + return rapid.Custom( + func(t *rapid.T) fn.Option[rfqmath.BigIntFixedPoint] { + if rapid.Bool().Draw(t, "present") { + return fn.Some( + fixedPointGen().Draw(t, "fp"), + ) + } + return fn.None[rfqmath.BigIntFixedPoint]() + }, + ) +} + +// assetIDGen draws a random 32-byte asset.ID. +func assetIDGen() *rapid.Generator[asset.ID] { + return rapid.Custom(func(t *rapid.T) asset.ID { + var id asset.ID + for i := range id { + id[i] = rapid.Byte().Draw(t, "byte") + } + return id + }) +} + +// peerGen draws a random 33-byte route.Vertex. +func peerGen() *rapid.Generator[route.Vertex] { + return rapid.Custom(func(t *rapid.T) route.Vertex { + var v route.Vertex + for i := range v { + v[i] = rapid.Byte().Draw(t, "byte") + } + return v + }) +} + +// TestBuyRequestWireRoundtripProperty checks that any valid +// BuyRequest survives a wire encode/decode roundtrip. +func TestBuyRequestWireRoundtripProperty(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + peer := peerGen().Draw(t, "peer") + id := assetIDGen().Draw(t, "id") + spec := asset.NewSpecifierFromId(id) + + maxAmt := rapid.Uint64Range(1, 1_000_000).Draw( + t, "maxAmt", + ) + minAmt := optionalUint64Gen(maxAmt).Draw( + t, "minAmt", + ) + rateLimit := optionalFixedPointGen().Draw( + t, "rateLimit", + ) + + noPolicy := fn.None[ExecutionPolicy]() + req, err := NewBuyRequest( + peer, spec, maxAmt, minAmt, + rateLimit, fn.None[AssetRate](), + "", noPolicy, + ) + require.NoError(t, err) + + wireMsg, err := req.ToWire() + require.NoError(t, err) + + var msgData requestWireMsgData + err = msgData.Decode( + bytes.NewReader(wireMsg.Data), + ) + require.NoError(t, err) + + decoded, err := NewBuyRequestFromWire( + wireMsg, msgData, + ) + require.NoError(t, err) + + // Max amount must be preserved. + require.Equal(t, maxAmt, decoded.AssetMaxAmt) + + // Min amount must match. + requireOptEq(t, minAmt, decoded.AssetMinAmt) + + // Rate limit must match via Cmp. + requireOptFpEq( + t, rateLimit, decoded.AssetRateLimit, + ) + }) +} + +// TestSellRequestWireRoundtripProperty checks that any valid +// SellRequest survives a wire encode/decode roundtrip. +func TestSellRequestWireRoundtripProperty(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + peer := peerGen().Draw(t, "peer") + id := assetIDGen().Draw(t, "id") + spec := asset.NewSpecifierFromId(id) + + maxAmt := rapid.Uint64Range(1, 1_000_000).Draw( + t, "maxAmt", + ) + minAmt := optionalMsatGen(maxAmt).Draw( + t, "minAmt", + ) + rateLimit := optionalFixedPointGen().Draw( + t, "rateLimit", + ) + + noPolicy := fn.None[ExecutionPolicy]() + req, err := NewSellRequest( + peer, spec, + lnwire.MilliSatoshi(maxAmt), minAmt, + rateLimit, fn.None[AssetRate](), + "", noPolicy, + ) + require.NoError(t, err) + + wireMsg, err := req.ToWire() + require.NoError(t, err) + + var msgData requestWireMsgData + err = msgData.Decode( + bytes.NewReader(wireMsg.Data), + ) + require.NoError(t, err) + + decoded, err := NewSellRequestFromWire( + wireMsg, msgData, + ) + require.NoError(t, err) + + require.Equal( + t, lnwire.MilliSatoshi(maxAmt), + decoded.PaymentMaxAmt, + ) + + requireOptMsatEq( + t, minAmt, decoded.PaymentMinAmt, + ) + + requireOptFpEq( + t, rateLimit, decoded.AssetRateLimit, + ) + }) +} + +// TestMinMaxConstraintProperty verifies that Validate accepts +// min <= max and rejects min > max for both buy and sell requests. +func TestMinMaxConstraintProperty(t *testing.T) { + t.Parallel() + + t.Run("buy_valid", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + maxAmt := rapid.Uint64Range(1, 1_000_000). + Draw(t, "max") + minAmt := rapid.Uint64Range(0, maxAmt). + Draw(t, "min") + + req := &BuyRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId( + asset.ID{1}, + ), + AssetMaxAmt: maxAmt, + AssetMinAmt: fn.Some(minAmt), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), //nolint:lll + AssetRateHint: fn.None[AssetRate](), + } + require.NoError(t, req.Validate()) + }) + }) + + t.Run("buy_invalid", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + maxAmt := rapid.Uint64Range( + 0, 1_000_000-1, + ).Draw(t, "max") + minAmt := rapid.Uint64Range( + maxAmt+1, 1_000_000, + ).Draw(t, "min") + + req := &BuyRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId( + asset.ID{1}, + ), + AssetMaxAmt: maxAmt, + AssetMinAmt: fn.Some(minAmt), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), //nolint:lll + AssetRateHint: fn.None[AssetRate](), + } + require.Error(t, req.Validate()) + }) + }) + + t.Run("sell_valid", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + maxAmt := rapid.Uint64Range(1, 1_000_000). + Draw(t, "max") + minAmt := rapid.Uint64Range(0, maxAmt). + Draw(t, "min") + + req := &SellRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId( + asset.ID{1}, + ), + PaymentMaxAmt: lnwire.MilliSatoshi( + maxAmt, + ), + PaymentMinAmt: fn.Some( + lnwire.MilliSatoshi(minAmt), + ), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), //nolint:lll + AssetRateHint: fn.None[AssetRate](), + } + require.NoError(t, req.Validate()) + }) + }) + + t.Run("sell_invalid", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + maxAmt := rapid.Uint64Range( + 0, 1_000_000-1, + ).Draw(t, "max") + minAmt := rapid.Uint64Range( + maxAmt+1, 1_000_000, + ).Draw(t, "min") + + req := &SellRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId( + asset.ID{1}, + ), + PaymentMaxAmt: lnwire.MilliSatoshi( + maxAmt, + ), + PaymentMinAmt: fn.Some( + lnwire.MilliSatoshi(minAmt), + ), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), //nolint:lll + AssetRateHint: fn.None[AssetRate](), + } + require.Error(t, req.Validate()) + }) + }) +} + +// TestNegativeRateLimitRejected verifies that Validate rejects a +// rate limit with a non-positive coefficient for both buy and sell +// requests. +func TestNegativeRateLimitRejected(t *testing.T) { + t.Parallel() + + spec := asset.NewSpecifierFromId(asset.ID{1}) + negLimit := rfqmath.FixedPoint[rfqmath.BigInt]{ + Coefficient: rfqmath.NewBigInt( + big.NewInt(-5), + ), + Scale: 0, + } + zeroLimit := rfqmath.FixedPoint[rfqmath.BigInt]{ + Coefficient: rfqmath.NewBigIntFromUint64(0), + Scale: 0, + } + + t.Run("buy_negative", func(t *testing.T) { + t.Parallel() + req := &BuyRequest{ + Version: V1, + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetRateLimit: fn.Some(negLimit), + AssetRateHint: fn.None[AssetRate](), + } + err := req.Validate() + require.ErrorContains( + t, err, "coefficient must be positive", + ) + }) + + t.Run("buy_zero", func(t *testing.T) { + t.Parallel() + req := &BuyRequest{ + Version: V1, + AssetSpecifier: spec, + AssetMaxAmt: 100, + AssetRateLimit: fn.Some(zeroLimit), + AssetRateHint: fn.None[AssetRate](), + } + err := req.Validate() + require.ErrorContains( + t, err, "coefficient must be positive", + ) + }) + + t.Run("sell_negative", func(t *testing.T) { + t.Parallel() + req := &SellRequest{ + Version: V1, + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + AssetRateLimit: fn.Some(negLimit), + AssetRateHint: fn.None[AssetRate](), + } + err := req.Validate() + require.ErrorContains( + t, err, "coefficient must be positive", + ) + }) + + t.Run("sell_zero", func(t *testing.T) { + t.Parallel() + req := &SellRequest{ + Version: V1, + AssetSpecifier: spec, + PaymentMaxAmt: 1000, + AssetRateLimit: fn.Some(zeroLimit), + AssetRateHint: fn.None[AssetRate](), + } + err := req.Validate() + require.ErrorContains( + t, err, "coefficient must be positive", + ) + }) +} + +// TestRateBoundEnforcementProperty verifies that rate limit fields +// survive a wire roundtrip and preserve the ordering relationship +// that checkRateBound relies on. For each request type we draw a +// random rate limit, encode/decode the request, then confirm that +// Cmp between an independently drawn accepted rate and the decoded +// limit yields the same result as comparing against the original. +func TestRateBoundEnforcementProperty(t *testing.T) { + t.Parallel() + + t.Run("buy", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + peer := peerGen().Draw(t, "peer") + id := assetIDGen().Draw(t, "id") + spec := asset.NewSpecifierFromId(id) + + maxAmt := rapid.Uint64Range(1, 1_000_000). + Draw(t, "maxAmt") + limit := fixedPointGen().Draw(t, "limit") + + noExec := fn.None[ExecutionPolicy]() + req, err := NewBuyRequest( + peer, spec, maxAmt, + fn.None[uint64](), + fn.Some(limit), + fn.None[AssetRate](), + "", noExec, + ) + require.NoError(t, err) + + wireMsg, err := req.ToWire() + require.NoError(t, err) + + var msgData requestWireMsgData + err = msgData.Decode( + bytes.NewReader(wireMsg.Data), + ) + require.NoError(t, err) + + decoded, err := NewBuyRequestFromWire( + wireMsg, msgData, + ) + require.NoError(t, err) + + decodedLimit, err := decoded.AssetRateLimit. + UnwrapOrErr( + errMissingRateLimit, + ) + require.NoError(t, err) + + // Ordering vs an independent rate must + // be identical before and after roundtrip. + accepted := fixedPointGen().Draw( + t, "accepted", + ) + require.Equal( + t, accepted.Cmp(limit), + accepted.Cmp(decodedLimit), + "buy Cmp mismatch after roundtrip", + ) + }) + }) + + t.Run("sell", func(t *testing.T) { + t.Parallel() + rapid.Check(t, func(t *rapid.T) { + peer := peerGen().Draw(t, "peer") + id := assetIDGen().Draw(t, "id") + spec := asset.NewSpecifierFromId(id) + + maxAmt := rapid.Uint64Range( + 1, 1_000_000, + ).Draw(t, "maxAmt") + limit := fixedPointGen().Draw(t, "limit") + + noExec := fn.None[ExecutionPolicy]() + req, err := NewSellRequest( + peer, spec, + lnwire.MilliSatoshi(maxAmt), + fn.None[lnwire.MilliSatoshi](), + fn.Some(limit), + fn.None[AssetRate](), + "", noExec, + ) + require.NoError(t, err) + + wireMsg, err := req.ToWire() + require.NoError(t, err) + + var msgData requestWireMsgData + err = msgData.Decode( + bytes.NewReader(wireMsg.Data), + ) + require.NoError(t, err) + + decoded, err := NewSellRequestFromWire( + wireMsg, msgData, + ) + require.NoError(t, err) + + decodedLimit, err := decoded.AssetRateLimit. + UnwrapOrErr( + errMissingRateLimit, + ) + require.NoError(t, err) + + accepted := fixedPointGen().Draw( + t, "accepted", + ) + require.Equal( + t, accepted.Cmp(limit), + accepted.Cmp(decodedLimit), + "sell Cmp mismatch after roundtrip", + ) + }) + }) +} + +// errMissingRateLimit is returned when a rate limit is expected +// but not present after wire roundtrip. +var errMissingRateLimit = fmt.Errorf("rate limit missing") + +// TestBuyRequestRoundtripWithHintProperty verifies that a BuyRequest +// with all fields (including AssetRateHint) survives a roundtrip. +func TestBuyRequestRoundtripWithHintProperty(t *testing.T) { + t.Parallel() + + rapid.Check(t, func(t *rapid.T) { + peer := peerGen().Draw(t, "peer") + id := assetIDGen().Draw(t, "id") + spec := asset.NewSpecifierFromId(id) + + maxAmt := rapid.Uint64Range(1, 1_000_000).Draw( + t, "maxAmt", + ) + minAmt := optionalUint64Gen(maxAmt).Draw( + t, "minAmt", + ) + rateLimit := optionalFixedPointGen().Draw( + t, "rateLimit", + ) + + // Always include a rate hint so we test the full + // field set. + expiry := time.Now().Add(5 * time.Minute).UTC() + fp := fixedPointGen().Draw(t, "hintRate") + hint := fn.Some(NewAssetRate(fp, expiry)) + + noPolicy := fn.None[ExecutionPolicy]() + req, err := NewBuyRequest( + peer, spec, maxAmt, minAmt, + rateLimit, hint, "", noPolicy, + ) + require.NoError(t, err) + + wireMsg, err := req.ToWire() + require.NoError(t, err) + + var msgData requestWireMsgData + err = msgData.Decode( + bytes.NewReader(wireMsg.Data), + ) + require.NoError(t, err) + + decoded, err := NewBuyRequestFromWire( + wireMsg, msgData, + ) + require.NoError(t, err) + + require.Equal(t, maxAmt, decoded.AssetMaxAmt) + requireOptEq(t, minAmt, decoded.AssetMinAmt) + requireOptFpEq( + t, rateLimit, decoded.AssetRateLimit, + ) + require.True(t, decoded.AssetRateHint.IsSome()) + }) +} + +// --- helpers --- + +// requireOptEq asserts two fn.Option[uint64] values are equal. +func requireOptEq(t require.TestingT, + want, got fn.Option[uint64]) { + + t.(*rapid.T).Helper() + + if want.IsNone() { + require.True(t, got.IsNone()) + return + } + + require.True(t, got.IsSome()) + + wantVal := want.UnwrapOr(0) + gotVal := got.UnwrapOr(0) + require.Equal(t, wantVal, gotVal) +} + +// requireOptMsatEq asserts two fn.Option[lnwire.MilliSatoshi] +// values are equal. +func requireOptMsatEq(t require.TestingT, + want, got fn.Option[lnwire.MilliSatoshi]) { + + t.(*rapid.T).Helper() + + if want.IsNone() { + require.True(t, got.IsNone()) + return + } + + require.True(t, got.IsSome()) + + wantVal := want.UnwrapOr(0) + gotVal := got.UnwrapOr(0) + require.Equal(t, wantVal, gotVal) +} + +// requireOptFpEq asserts two optional BigIntFixedPoint values are +// equal via Cmp. +func requireOptFpEq(t require.TestingT, + want, got fn.Option[rfqmath.BigIntFixedPoint]) { + + if want.IsNone() { + require.True(t, got.IsNone()) + return + } + + require.True(t, got.IsSome()) + + wantVal := want.UnwrapOr( + rfqmath.NewBigIntFixedPoint(0, 0), + ) + gotVal := got.UnwrapOr( + rfqmath.NewBigIntFixedPoint(0, 0), + ) + require.Equal(t, 0, gotVal.Cmp(wantVal)) +} diff --git a/rfqmsg/request_test.go b/rfqmsg/request_test.go index 017e3e583..39c98fa32 100644 --- a/rfqmsg/request_test.go +++ b/rfqmsg/request_test.go @@ -34,6 +34,8 @@ type testCaseEncodeDecode struct { minInAsset *uint64 minOutAsset *uint64 + assetRateLimit *uint64 + oracleMetadata string } @@ -109,6 +111,16 @@ func (tc testCaseEncodeDecode) Request() requestWireMsgData { ) } + var assetRateLimit requestAssetRateLimit + if tc.assetRateLimit != nil { + rate := NewTlvFixedPointFromUint64( + *tc.assetRateLimit, 3, + ) + assetRateLimit = tlv.SomeRecordT[tlv.TlvType29]( + tlv.NewRecordT[tlv.TlvType29](rate), + ) + } + var oracleMetadata requestOracleMetadata if tc.oracleMetadata != "" { oracleMetadata = tlv.SomeRecordT[tlv.TlvType27]( @@ -131,6 +143,7 @@ func (tc testCaseEncodeDecode) Request() requestWireMsgData { OutAssetRateHint: outAssetRateHint, MinInAsset: minInAsset, MinOutAsset: minOutAsset, + AssetRateLimit: assetRateLimit, PriceOracleMetadata: oracleMetadata, } } @@ -162,6 +175,7 @@ func TestRequestMsgDataEncodeDecode(t *testing.T) { minInAsset := uint64(1) minOutAsset := uint64(10) + assetRateLimit := uint64(50000) testCases := []testCaseEncodeDecode{ { @@ -180,6 +194,7 @@ func TestRequestMsgDataEncodeDecode(t *testing.T) { outAssetRateHint: &outAssetRateHint, minInAsset: &minInAsset, minOutAsset: &minOutAsset, + assetRateLimit: &assetRateLimit, oracleMetadata: "this could be a JSON string", }, { diff --git a/rfqmsg/sell_request.go b/rfqmsg/sell_request.go index 4a581e71c..870a9dbcf 100644 --- a/rfqmsg/sell_request.go +++ b/rfqmsg/sell_request.go @@ -7,6 +7,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/lightninglabs/taproot-assets/asset" "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/tlv" @@ -52,6 +53,21 @@ type SellRequest struct { // must agree to pay. PaymentMaxAmt lnwire.MilliSatoshi + // PaymentMinAmt is an optional minimum msat amount for the quote. + // When set, the responding peer must agree to pay at least this + // much. + PaymentMinAmt fn.Option[lnwire.MilliSatoshi] + + // AssetRateLimit is an optional maximum acceptable rate (asset + // units per BTC). The seller sets a ceiling: "I won't give more + // than X units per BTC." + AssetRateLimit fn.Option[rfqmath.BigIntFixedPoint] + + // ExecutionPolicy is an optional execution policy for the + // quote request. IOC (default) accepts partial fills; FOK + // requires the full max amount to be viable. + ExecutionPolicy fn.Option[ExecutionPolicy] + // AssetRateHint represents a proposed conversion rate between the // subject asset and BTC. This rate is an initial suggestion intended to // initiate the RFQ negotiation process and may differ from the final @@ -69,8 +85,12 @@ type SellRequest struct { // NewSellRequest creates a new asset sell quote request. func NewSellRequest(peer route.Vertex, assetSpecifier asset.Specifier, - paymentMaxAmt lnwire.MilliSatoshi, assetRateHint fn.Option[AssetRate], - oracleMetadata string) (*SellRequest, error) { + paymentMaxAmt lnwire.MilliSatoshi, + paymentMinAmt fn.Option[lnwire.MilliSatoshi], + assetRateLimit fn.Option[rfqmath.BigIntFixedPoint], + assetRateHint fn.Option[AssetRate], + oracleMetadata string, + execPolicy fn.Option[ExecutionPolicy]) (*SellRequest, error) { id, err := NewID() if err != nil { @@ -83,15 +103,25 @@ func NewSellRequest(peer route.Vertex, assetSpecifier asset.Specifier, "length of %d bytes", MaxOracleMetadataLength) } - return &SellRequest{ + req := &SellRequest{ Peer: peer, Version: latestSellRequestVersion, ID: id, AssetSpecifier: assetSpecifier, PaymentMaxAmt: paymentMaxAmt, + PaymentMinAmt: paymentMinAmt, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, AssetRateHint: assetRateHint, PriceOracleMetadata: oracleMetadata, - }, nil + } + + if err := req.Validate(); err != nil { + return nil, fmt.Errorf("unable to validate sell "+ + "request: %w", err) + } + + return req, nil } // NewSellRequestFromWire instantiates a new instance from a wire message. @@ -147,6 +177,35 @@ func NewSellRequestFromWire(wireMsg WireMessage, }, ) + // Extract optional min payment amount. + var paymentMinAmt fn.Option[lnwire.MilliSatoshi] + msgData.MinOutAsset.WhenSome( + func(r tlv.RecordT[tlv.TlvType25, uint64]) { + if r.Val > 0 { + paymentMinAmt = fn.Some( + lnwire.MilliSatoshi(r.Val), + ) + } + }, + ) + + // Extract optional rate limit. + var assetRateLimit fn.Option[rfqmath.BigIntFixedPoint] + msgData.AssetRateLimit.WhenSome( + func(r tlv.RecordT[tlv.TlvType29, TlvFixedPoint]) { + fp := r.Val.IntoBigIntFixedPoint() + assetRateLimit = fn.Some(fp) + }, + ) + + // Extract optional execution policy. + var execPolicy fn.Option[ExecutionPolicy] + msgData.ExecutionPolicy.WhenSome( + func(r tlv.RecordT[tlv.TlvType31, uint8]) { + execPolicy = fn.Some(ExecutionPolicy(r.Val)) + }, + ) + req := SellRequest{ Peer: wireMsg.Peer, Version: msgData.Version.Val, @@ -155,7 +214,10 @@ func NewSellRequestFromWire(wireMsg WireMessage, PaymentMaxAmt: lnwire.MilliSatoshi( msgData.MaxInAsset.Val, ), - AssetRateHint: assetRateHint, + PaymentMinAmt: paymentMinAmt, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, + AssetRateHint: assetRateHint, } msgData.PriceOracleMetadata.ValOpt().WhenSome(func(metaBytes []byte) { @@ -191,6 +253,53 @@ func (q *SellRequest) Validate() error { "length of %d bytes", MaxOracleMetadataLength) } + // Ensure min <= max when min is set. + err = fn.MapOptionZ( + q.PaymentMinAmt, + func(minAmt lnwire.MilliSatoshi) error { + if minAmt > q.PaymentMaxAmt { + return fmt.Errorf("payment min amount "+ + "(%d) exceeds max amount (%d)", + minAmt, q.PaymentMaxAmt) + } + return nil + }, + ) + if err != nil { + return err + } + + // Ensure execution policy is valid when set. + err = fn.MapOptionZ( + q.ExecutionPolicy, + func(p ExecutionPolicy) error { + if p > ExecutionPolicyFOK { + return fmt.Errorf("invalid execution "+ + "policy: %d", p) + } + return nil + }, + ) + if err != nil { + return err + } + + // Ensure rate limit is strictly positive when set. + err = fn.MapOptionZ( + q.AssetRateLimit, + func(limit rfqmath.BigIntFixedPoint) error { + zero := rfqmath.NewBigIntFromUint64(0) + if !limit.Coefficient.Gt(zero) { + return fmt.Errorf("asset rate limit " + + "coefficient must be positive") + } + return nil + }, + ) + if err != nil { + return err + } + return nil } @@ -246,10 +355,32 @@ func (q *SellRequest) String() string { }, ) + minAmtStr := fn.MapOptionZ( + q.PaymentMinAmt, + func(v lnwire.MilliSatoshi) string { + return fmt.Sprintf(", payment_min_amt=%d", v) + }, + ) + + rateLimitStr := fn.MapOptionZ( + q.AssetRateLimit, + func(v rfqmath.BigIntFixedPoint) string { + return fmt.Sprintf(", asset_rate_limit=%s", + v.String()) + }, + ) + + execPolicyStr := fn.MapOptionZ( + q.ExecutionPolicy, + func(p ExecutionPolicy) string { + return fmt.Sprintf(", exec_policy=%d", p) + }, + ) + return fmt.Sprintf("SellRequest(peer=%x, id=%x, asset=%s, "+ - "payment_max_amt=%d, asset_rate_hint=%s)", + "payment_max_amt=%d%s%s%s, asset_rate_hint=%s)", q.Peer[:], q.ID[:], q.AssetSpecifier.String(), q.PaymentMaxAmt, - assetRateHintStr) + minAmtStr, rateLimitStr, execPolicyStr, assetRateHintStr) } // Ensure that the message type implements the OutgoingMsg interface. diff --git a/rfqmsg/sell_request_test.go b/rfqmsg/sell_request_test.go new file mode 100644 index 000000000..09d25ff71 --- /dev/null +++ b/rfqmsg/sell_request_test.go @@ -0,0 +1,284 @@ +package rfqmsg + +import ( + "bytes" + "testing" + "time" + + "github.com/lightninglabs/taproot-assets/asset" + "github.com/lightninglabs/taproot-assets/fn" + "github.com/lightninglabs/taproot-assets/rfqmath" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" + "github.com/stretchr/testify/require" +) + +// sellRequestRoundtrip encodes a SellRequest to wire and decodes it +// back, returning the decoded request. +func sellRequestRoundtrip(t *testing.T, + req *SellRequest) *SellRequest { + + t.Helper() + + wireMsg, err := req.ToWire() + require.NoError(t, err) + + var msgData requestWireMsgData + err = msgData.Decode(bytes.NewReader(wireMsg.Data)) + require.NoError(t, err) + + decoded, err := NewSellRequestFromWire(wireMsg, msgData) + require.NoError(t, err) + + return decoded +} + +// TestSellRequestMinAmtRoundtrip verifies that PaymentMinAmt survives +// a wire roundtrip. +func TestSellRequestMinAmtRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x01} + spec := asset.NewSpecifierFromId(asset.ID{0xAA}) + + req, err := NewSellRequest( + peer, spec, lnwire.MilliSatoshi(5000), + fn.Some(lnwire.MilliSatoshi(1000)), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := sellRequestRoundtrip(t, req) + + require.True(t, decoded.PaymentMinAmt.IsSome()) + decoded.PaymentMinAmt.WhenSome(func(v lnwire.MilliSatoshi) { + require.Equal(t, lnwire.MilliSatoshi(1000), v) + }) + require.Equal( + t, lnwire.MilliSatoshi(5000), decoded.PaymentMaxAmt, + ) +} + +// TestSellRequestRateLimitRoundtrip verifies that AssetRateLimit +// survives a wire roundtrip. +func TestSellRequestRateLimitRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x02} + spec := asset.NewSpecifierFromId(asset.ID{0xBB}) + limit := rfqmath.NewBigIntFixedPoint(75000, 2) + + req, err := NewSellRequest( + peer, spec, lnwire.MilliSatoshi(10000), + fn.None[lnwire.MilliSatoshi](), + fn.Some(limit), fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := sellRequestRoundtrip(t, req) + + require.True(t, decoded.AssetRateLimit.IsSome()) + decoded.AssetRateLimit.WhenSome( + func(v rfqmath.BigIntFixedPoint) { + require.Equal(t, 0, v.Cmp(limit)) + }, + ) +} + +// TestSellRequestNoOptionalFieldsRoundtrip verifies backward +// compatibility when no optional fields are set. +func TestSellRequestNoOptionalFieldsRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x03} + spec := asset.NewSpecifierFromId(asset.ID{0xCC}) + + req, err := NewSellRequest( + peer, spec, lnwire.MilliSatoshi(3000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := sellRequestRoundtrip(t, req) + + require.True(t, decoded.PaymentMinAmt.IsNone()) + require.True(t, decoded.AssetRateLimit.IsNone()) + require.Equal( + t, lnwire.MilliSatoshi(3000), decoded.PaymentMaxAmt, + ) +} + +// TestSellRequestAllFieldsRoundtrip verifies that all optional fields +// survive a roundtrip together. +func TestSellRequestAllFieldsRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x04} + spec := asset.NewSpecifierFromId(asset.ID{0xDD}) + limit := rfqmath.NewBigIntFixedPoint(88000, 3) + expiry := time.Now().Add(5 * time.Minute).UTC() + rateHint := NewAssetRate( + rfqmath.NewBigIntFixedPoint(100000, 0), expiry, + ) + + req, err := NewSellRequest( + peer, spec, lnwire.MilliSatoshi(9000), + fn.Some(lnwire.MilliSatoshi(2000)), + fn.Some(limit), fn.Some(rateHint), "sell-meta", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := sellRequestRoundtrip(t, req) + + require.True(t, decoded.PaymentMinAmt.IsSome()) + decoded.PaymentMinAmt.WhenSome(func(v lnwire.MilliSatoshi) { + require.Equal(t, lnwire.MilliSatoshi(2000), v) + }) + + require.True(t, decoded.AssetRateLimit.IsSome()) + decoded.AssetRateLimit.WhenSome( + func(v rfqmath.BigIntFixedPoint) { + require.Equal(t, 0, v.Cmp(limit)) + }, + ) + + require.True(t, decoded.AssetRateHint.IsSome()) + require.Equal(t, "sell-meta", decoded.PriceOracleMetadata) +} + +// TestSellRequestValidateMinGtMax ensures Validate rejects min > max. +func TestSellRequestValidateMinGtMax(t *testing.T) { + t.Parallel() + + req := &SellRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + PaymentMaxAmt: lnwire.MilliSatoshi(100), + PaymentMinAmt: fn.Some( + lnwire.MilliSatoshi(200), + ), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), + AssetRateHint: fn.None[AssetRate](), + } + + err := req.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "exceeds max amount") +} + +// TestSellRequestValidateMinEqMax ensures min == max is valid. +func TestSellRequestValidateMinEqMax(t *testing.T) { + t.Parallel() + + req := &SellRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + PaymentMaxAmt: lnwire.MilliSatoshi(500), + PaymentMinAmt: fn.Some( + lnwire.MilliSatoshi(500), + ), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), + AssetRateHint: fn.None[AssetRate](), + } + + err := req.Validate() + require.NoError(t, err) +} + +// TestSellRequestValidateZeroRateLimit ensures a zero rate limit +// coefficient is rejected. +func TestSellRequestValidateZeroRateLimit(t *testing.T) { + t.Parallel() + + zeroLimit := rfqmath.NewBigIntFixedPoint(0, 0) + req := &SellRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + PaymentMaxAmt: lnwire.MilliSatoshi(1000), + PaymentMinAmt: fn.None[lnwire.MilliSatoshi](), + AssetRateLimit: fn.Some(zeroLimit), + AssetRateHint: fn.None[AssetRate](), + } + + err := req.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "must be positive") +} + +// TestSellRequestExecutionPolicyRoundtrip verifies that a FOK +// execution policy survives a wire roundtrip. +func TestSellRequestExecutionPolicyRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x05} + spec := asset.NewSpecifierFromId(asset.ID{0xEE}) + + req, err := NewSellRequest( + peer, spec, lnwire.MilliSatoshi(7000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.Some(ExecutionPolicyFOK), + ) + require.NoError(t, err) + + decoded := sellRequestRoundtrip(t, req) + + require.True(t, decoded.ExecutionPolicy.IsSome()) + decoded.ExecutionPolicy.WhenSome( + func(v ExecutionPolicy) { + require.Equal(t, ExecutionPolicyFOK, v) + }, + ) +} + +// TestSellRequestNoExecutionPolicyRoundtrip verifies that an +// absent execution policy stays None after a wire roundtrip. +func TestSellRequestNoExecutionPolicyRoundtrip(t *testing.T) { + t.Parallel() + + peer := route.Vertex{0x06} + spec := asset.NewSpecifierFromId(asset.ID{0xFF}) + + req, err := NewSellRequest( + peer, spec, lnwire.MilliSatoshi(8000), + fn.None[lnwire.MilliSatoshi](), + fn.None[rfqmath.BigIntFixedPoint](), + fn.None[AssetRate](), "", + fn.None[ExecutionPolicy](), + ) + require.NoError(t, err) + + decoded := sellRequestRoundtrip(t, req) + + require.True(t, decoded.ExecutionPolicy.IsNone()) +} + +// TestSellRequestInvalidExecutionPolicy ensures Validate rejects +// an unknown execution policy value. +func TestSellRequestInvalidExecutionPolicy(t *testing.T) { + t.Parallel() + + req := &SellRequest{ + Version: V1, + AssetSpecifier: asset.NewSpecifierFromId(asset.ID{1}), + PaymentMaxAmt: lnwire.MilliSatoshi(1000), + PaymentMinAmt: fn.None[lnwire.MilliSatoshi](), + AssetRateLimit: fn.None[rfqmath.BigIntFixedPoint](), + AssetRateHint: fn.None[AssetRate](), + ExecutionPolicy: fn.Some( + ExecutionPolicy(2), + ), + } + + err := req.Validate() + require.Error(t, err) + require.Contains(t, err.Error(), "execution policy") +} diff --git a/rpcserver/rpcserver.go b/rpcserver/rpcserver.go index 77e688a04..b7176db2e 100644 --- a/rpcserver/rpcserver.go +++ b/rpcserver/rpcserver.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "math" + "math/big" "net/http" "net/url" "sort" @@ -8324,7 +8325,55 @@ func parseAssetSpecifier(reqAssetID []byte, reqAssetIDStr string, return assetID, groupKey, nil } -// unmarshalAssetBuyOrder unmarshals an asset buy order from the RPC form. +// unmarshalOptionalFixedPoint converts an optional RPC FixedPoint into +// an fn.Option[rfqmath.BigIntFixedPoint]. A nil input returns fn.None. +func unmarshalOptionalFixedPoint( + fp *rfqrpc.FixedPoint) (fn.Option[rfqmath.BigIntFixedPoint], error) { + + if fp == nil { + return fn.None[rfqmath.BigIntFixedPoint](), nil + } + + if fp.Scale > 255 { + return fn.None[rfqmath.BigIntFixedPoint](), + fmt.Errorf("scale value overflow: %v", fp.Scale) + } + + cBigInt := new(big.Int) + if _, ok := cBigInt.SetString(fp.Coefficient, 10); !ok { + return fn.None[rfqmath.BigIntFixedPoint](), + fmt.Errorf("invalid coefficient: %s", + fp.Coefficient) + } + + result := rfqmath.BigIntFixedPoint{ + Coefficient: rfqmath.NewBigInt(cBigInt), + Scale: uint8(fp.Scale), + } + + return fn.Some(result), nil +} + +// unmarshalExecutionPolicy converts the RPC execution policy enum to +// the internal optional type. IOC (0/default) maps to fn.None. FOK +// maps to fn.Some(ExecutionPolicyFOK). Unknown values are rejected. +func unmarshalExecutionPolicy( + p rfqrpc.ExecutionPolicy, +) (fn.Option[rfqmsg.ExecutionPolicy], error) { + + switch p { + case rfqrpc.ExecutionPolicy_EXECUTION_POLICY_IOC: + return fn.None[rfqmsg.ExecutionPolicy](), nil + + case rfqrpc.ExecutionPolicy_EXECUTION_POLICY_FOK: + return fn.Some(rfqmsg.ExecutionPolicyFOK), nil + + default: + return fn.None[rfqmsg.ExecutionPolicy](), + fmt.Errorf("unknown execution policy: %v", p) + } +} + func unmarshalAssetBuyOrder( req *rfqrpc.AddAssetBuyOrderRequest) (*rfq.BuyOrder, error) { @@ -8370,9 +8419,35 @@ func unmarshalAssetBuyOrder( len(req.PriceOracleMetadata)) } + // Unmarshal optional min amount. + var assetMinAmt fn.Option[uint64] + if req.AssetMinAmt > 0 { + assetMinAmt = fn.Some(req.AssetMinAmt) + } + + // Unmarshal optional rate limit. + assetRateLimit, err := unmarshalOptionalFixedPoint( + req.AssetRateLimit, + ) + if err != nil { + return nil, fmt.Errorf("error unmarshalling asset rate "+ + "limit: %w", err) + } + + // Unmarshal optional execution policy. + execPolicy, err := unmarshalExecutionPolicy( + req.ExecutionPolicy, + ) + if err != nil { + return nil, err + } + return &rfq.BuyOrder{ AssetSpecifier: assetSpecifier, AssetMaxAmt: req.AssetMaxAmt, + AssetMinAmt: assetMinAmt, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, Expiry: expiry, Peer: fn.MaybeSome(peer), PriceOracleMetadata: req.PriceOracleMetadata, @@ -8586,9 +8661,37 @@ func unmarshalAssetSellOrder( len(req.PriceOracleMetadata)) } + // Unmarshal optional min payment amount. + var paymentMinAmt fn.Option[lnwire.MilliSatoshi] + if req.PaymentMinAmt > 0 { + paymentMinAmt = fn.Some( + lnwire.MilliSatoshi(req.PaymentMinAmt), + ) + } + + // Unmarshal optional rate limit. + assetRateLimit, err := unmarshalOptionalFixedPoint( + req.AssetRateLimit, + ) + if err != nil { + return nil, fmt.Errorf("error unmarshalling asset rate "+ + "limit: %w", err) + } + + // Unmarshal optional execution policy. + execPolicy, err := unmarshalExecutionPolicy( + req.ExecutionPolicy, + ) + if err != nil { + return nil, err + } + return &rfq.SellOrder{ AssetSpecifier: assetSpecifier, PaymentMaxAmt: lnwire.MilliSatoshi(req.PaymentMaxAmt), + PaymentMinAmt: paymentMinAmt, + AssetRateLimit: assetRateLimit, + ExecutionPolicy: execPolicy, Expiry: expiry, Peer: peer, PriceOracleMetadata: req.PriceOracleMetadata, diff --git a/taprpc/portfoliopilotrpc/portfolio_pilot.pb.go b/taprpc/portfoliopilotrpc/portfolio_pilot.pb.go index fabfd1afd..1de1b67b8 100644 --- a/taprpc/portfoliopilotrpc/portfolio_pilot.pb.go +++ b/taprpc/portfoliopilotrpc/portfolio_pilot.pb.go @@ -187,6 +187,12 @@ const ( // REJECT_CODE_PRICE_ORACLE_UNAVAILABLE indicates that pricing could not be // provided due to an unavailable oracle. RejectCode_REJECT_CODE_PRICE_ORACLE_UNAVAILABLE RejectCode = 1 + // REJECT_CODE_MIN_FILL_NOT_MET indicates that the minimum fill + // constraint was not satisfiable at the accepted rate. + RejectCode_REJECT_CODE_MIN_FILL_NOT_MET RejectCode = 2 + // REJECT_CODE_PRICE_BOUND_MISS indicates that the accepted rate + // violated the requester's rate limit constraint. + RejectCode_REJECT_CODE_PRICE_BOUND_MISS RejectCode = 3 ) // Enum value maps for RejectCode. @@ -194,10 +200,14 @@ var ( RejectCode_name = map[int32]string{ 0: "REJECT_CODE_UNSPECIFIED", 1: "REJECT_CODE_PRICE_ORACLE_UNAVAILABLE", + 2: "REJECT_CODE_MIN_FILL_NOT_MET", + 3: "REJECT_CODE_PRICE_BOUND_MISS", } RejectCode_value = map[string]int32{ "REJECT_CODE_UNSPECIFIED": 0, "REJECT_CODE_PRICE_ORACLE_UNAVAILABLE": 1, + "REJECT_CODE_MIN_FILL_NOT_MET": 2, + "REJECT_CODE_PRICE_BOUND_MISS": 3, } ) @@ -247,6 +257,15 @@ const ( // VALID_ACCEPT_QUOTE indicates that the accepted quote passed all // validation checks successfully. QuoteRespStatus_VALID_ACCEPT_QUOTE QuoteRespStatus = 4 + // MIN_FILL_NOT_MET indicates that the minimum fill constraint was + // not satisfiable at the accepted rate. + QuoteRespStatus_MIN_FILL_NOT_MET QuoteRespStatus = 5 + // RATE_BOUND_MISS indicates that the accepted rate violated the + // requester's rate limit constraint. + QuoteRespStatus_RATE_BOUND_MISS QuoteRespStatus = 6 + // FOK_NOT_VIABLE indicates that the FOK execution policy could + // not be satisfied at the accepted rate. + QuoteRespStatus_FOK_NOT_VIABLE QuoteRespStatus = 7 ) // Enum value maps for QuoteRespStatus. @@ -257,6 +276,9 @@ var ( 2: "PRICE_ORACLE_QUERY_ERR", 3: "PORTFOLIO_PILOT_ERR", 4: "VALID_ACCEPT_QUOTE", + 5: "MIN_FILL_NOT_MET", + 6: "RATE_BOUND_MISS", + 7: "FOK_NOT_VIABLE", } QuoteRespStatus_value = map[string]int32{ "INVALID_ASSET_RATES": 0, @@ -264,6 +286,9 @@ var ( "PRICE_ORACLE_QUERY_ERR": 2, "PORTFOLIO_PILOT_ERR": 3, "VALID_ACCEPT_QUOTE": 4, + "MIN_FILL_NOT_MET": 5, + "RATE_BOUND_MISS": 6, + "FOK_NOT_VIABLE": 7, } ) @@ -626,6 +651,12 @@ type BuyRequest struct { PriceOracleMetadata string `protobuf:"bytes,4,opt,name=price_oracle_metadata,json=priceOracleMetadata,proto3" json:"price_oracle_metadata,omitempty"` // peer_id is the 33-byte public key of the counterparty peer. PeerId []byte `protobuf:"bytes,5,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + // asset_min_amount is an optional minimum asset amount. A value of 0 + // means unset. + AssetMinAmount uint64 `protobuf:"varint,6,opt,name=asset_min_amount,json=assetMinAmount,proto3" json:"asset_min_amount,omitempty"` + // asset_rate_limit is an optional minimum acceptable rate (asset units + // per BTC). + AssetRateLimit *FixedPoint `protobuf:"bytes,7,opt,name=asset_rate_limit,json=assetRateLimit,proto3" json:"asset_rate_limit,omitempty"` } func (x *BuyRequest) Reset() { @@ -695,6 +726,20 @@ func (x *BuyRequest) GetPeerId() []byte { return nil } +func (x *BuyRequest) GetAssetMinAmount() uint64 { + if x != nil { + return x.AssetMinAmount + } + return 0 +} + +func (x *BuyRequest) GetAssetRateLimit() *FixedPoint { + if x != nil { + return x.AssetRateLimit + } + return nil +} + // SellRequest represents a request to sell the subject asset. type SellRequest struct { state protoimpl.MessageState @@ -712,6 +757,12 @@ type SellRequest struct { PriceOracleMetadata string `protobuf:"bytes,4,opt,name=price_oracle_metadata,json=priceOracleMetadata,proto3" json:"price_oracle_metadata,omitempty"` // peer_id is the 33-byte public key of the counterparty peer. PeerId []byte `protobuf:"bytes,5,opt,name=peer_id,json=peerId,proto3" json:"peer_id,omitempty"` + // payment_min_amount is an optional minimum msat amount. A value of 0 + // means unset. + PaymentMinAmount uint64 `protobuf:"varint,6,opt,name=payment_min_amount,json=paymentMinAmount,proto3" json:"payment_min_amount,omitempty"` + // asset_rate_limit is an optional maximum acceptable rate (asset units + // per BTC). + AssetRateLimit *FixedPoint `protobuf:"bytes,7,opt,name=asset_rate_limit,json=assetRateLimit,proto3" json:"asset_rate_limit,omitempty"` } func (x *SellRequest) Reset() { @@ -781,6 +832,20 @@ func (x *SellRequest) GetPeerId() []byte { return nil } +func (x *SellRequest) GetPaymentMinAmount() uint64 { + if x != nil { + return x.PaymentMinAmount + } + return 0 +} + +func (x *SellRequest) GetAssetRateLimit() *FixedPoint { + if x != nil { + return x.AssetRateLimit + } + return nil +} + // ResolveRequestRequest specifies a quote request to resolve. type ResolveRequestRequest struct { state protoimpl.MessageState @@ -1355,7 +1420,7 @@ var file_portfoliopilotrpc_portfolio_pilot_proto_rawDesc = []byte{ 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x04, 0x72, 0x61, 0x74, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x95, 0x02, 0x0a, 0x0a, 0x42, 0x75, 0x79, 0x52, 0x65, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x88, 0x03, 0x0a, 0x0a, 0x42, 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, @@ -1372,170 +1437,193 @@ var file_portfoliopilotrpc_portfolio_pilot_proto_rawDesc = []byte{ 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x22, 0x9a, - 0x02, 0x0a, 0x0b, 0x53, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, - 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, - 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x4d, - 0x61, 0x78, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, - 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x52, - 0x0d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x12, 0x32, - 0x0a, 0x15, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x5f, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, - 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x22, 0xa9, 0x01, 0x0a, 0x15, - 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x40, 0x0a, 0x0b, 0x62, 0x75, 0x79, 0x5f, 0x72, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, - 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x62, 0x75, 0x79, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 0x5f, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, - 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, - 0x63, 0x2e, 0x53, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, - 0x0b, 0x73, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, - 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x92, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, - 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x12, 0x36, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, - 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, - 0x48, 0x00, 0x52, 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x12, 0x36, 0x0a, 0x06, 0x72, 0x65, - 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x65, 0x6a, 0x65, 0x63, 0x74, 0x45, 0x72, 0x72, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x6a, 0x65, - 0x63, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0xfd, 0x01, 0x0a, - 0x0d, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x17, - 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x41, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, - 0x74, 0x65, 0x64, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, - 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, - 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x52, 0x0c, 0x61, 0x63, - 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x52, 0x61, 0x74, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x62, 0x75, - 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1d, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, - 0x52, 0x0a, 0x62, 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x43, 0x0a, 0x0c, - 0x73, 0x65, 0x6c, 0x6c, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, - 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x42, 0x09, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x54, 0x0a, 0x18, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x28, + 0x0a, 0x10, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x4d, + 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x10, 0x61, 0x73, 0x73, 0x65, + 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, + 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x50, 0x6f, 0x69, 0x6e, + 0x74, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, + 0x74, 0x22, 0x91, 0x03, 0x0a, 0x0b, 0x53, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x4a, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, + 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x6f, 0x72, + 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x2c, 0x0a, + 0x12, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x6d, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x4d, 0x61, 0x78, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x0f, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, + 0x74, 0x65, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x48, 0x69, 0x6e, + 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x72, 0x61, 0x63, 0x6c, + 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x13, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x2c, + 0x0a, 0x12, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x61, 0x6d, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x4d, 0x69, 0x6e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x47, 0x0a, 0x10, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, + 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, + 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, + 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x22, 0xa9, 0x01, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, + 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x40, 0x0a, 0x0b, 0x62, 0x75, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x62, 0x75, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, + 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x65, 0x6c, 0x6c, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x73, 0x65, 0x6c, 0x6c, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x22, 0x92, 0x01, 0x0a, 0x16, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x06, + 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, + 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x48, 0x00, 0x52, 0x06, 0x61, 0x63, + 0x63, 0x65, 0x70, 0x74, 0x12, 0x36, 0x0a, 0x06, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, + 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x45, + 0x72, 0x72, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x42, 0x08, 0x0a, 0x06, + 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x22, 0xfd, 0x01, 0x0a, 0x0d, 0x41, 0x63, 0x63, 0x65, 0x70, + 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, + 0x64, 0x12, 0x41, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x72, 0x61, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, + 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, + 0x52, 0x61, 0x74, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x62, 0x75, 0x79, 0x5f, 0x72, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x70, 0x6f, 0x72, 0x74, + 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x42, 0x75, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0a, 0x62, 0x75, 0x79, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x43, 0x0a, 0x0c, 0x73, 0x65, 0x6c, 0x6c, 0x5f, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x70, + 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, + 0x2e, 0x53, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, + 0x73, 0x65, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0x54, 0x0a, 0x18, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, + 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, + 0x75, 0x6f, 0x74, 0x65, 0x52, 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x22, 0x57, 0x0a, 0x19, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x38, 0x0a, 0x06, 0x61, 0x63, 0x63, 0x65, - 0x70, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x63, 0x63, - 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x06, 0x61, 0x63, 0x63, 0x65, - 0x70, 0x74, 0x22, 0x57, 0x0a, 0x19, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x41, 0x63, 0x63, 0x65, - 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x3a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, - 0x22, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xe8, 0x03, 0x0a, 0x16, - 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4a, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, - 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x21, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, - 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, - 0x65, 0x72, 0x12, 0x47, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x29, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x06, 0x69, - 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x70, 0x6f, + 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x70, 0x6f, 0x72, 0x74, + 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, + 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0xe8, 0x03, 0x0a, 0x16, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, + 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x4a, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x70, 0x6f, 0x72, 0x74, + 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, 0x73, + 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x47, 0x0a, 0x09, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x29, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, + 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x0a, 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x19, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, + 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, + 0x52, 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, + 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, + 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x04, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x41, 0x6d, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, + 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, - 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x69, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x21, - 0x0a, 0x0c, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, - 0x74, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x61, 0x6d, 0x6f, - 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, - 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x68, 0x69, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, - 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x52, - 0x0d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x12, 0x32, - 0x0a, 0x15, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x5f, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, - 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, - 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x65, - 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x56, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, - 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, - 0x61, 0x74, 0x65, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x2a, 0x87, - 0x01, 0x0a, 0x16, 0x41, 0x73, 0x73, 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, - 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x24, 0x41, 0x53, 0x53, - 0x45, 0x54, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, - 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, 0x1c, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x52, 0x41, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x52, 0x0d, 0x61, 0x73, 0x73, 0x65, 0x74, + 0x52, 0x61, 0x74, 0x65, 0x48, 0x69, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x69, 0x63, + 0x65, 0x5f, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, + 0x61, 0x63, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, + 0x70, 0x65, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x70, + 0x65, 0x65, 0x72, 0x49, 0x64, 0x12, 0x29, 0x0a, 0x10, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, + 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x22, 0x56, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, + 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x0a, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, + 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x52, 0x09, 0x61, + 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x2a, 0x87, 0x01, 0x0a, 0x16, 0x41, 0x73, 0x73, + 0x65, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x24, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, - 0x42, 0x55, 0x59, 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x54, - 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x53, 0x45, 0x4c, 0x4c, 0x10, 0x02, 0x2a, 0xcd, 0x01, 0x0a, 0x06, 0x49, 0x6e, 0x74, - 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x49, - 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, - 0x45, 0x5f, 0x48, 0x49, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, - 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x02, - 0x12, 0x1e, 0x0a, 0x1a, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, - 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x59, 0x10, 0x03, - 0x12, 0x1c, 0x0a, 0x18, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, - 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x48, 0x49, 0x4e, 0x54, 0x10, 0x04, 0x12, 0x17, - 0x0a, 0x13, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, - 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x49, 0x4e, 0x54, 0x45, 0x4e, - 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x51, - 0x55, 0x41, 0x4c, 0x49, 0x46, 0x59, 0x10, 0x06, 0x2a, 0x53, 0x0a, 0x0a, 0x52, 0x65, 0x6a, 0x65, - 0x63, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x4a, 0x45, 0x43, 0x54, - 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, - 0x44, 0x10, 0x00, 0x12, 0x28, 0x0a, 0x24, 0x52, 0x45, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, - 0x44, 0x45, 0x5f, 0x50, 0x52, 0x49, 0x43, 0x45, 0x5f, 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, - 0x55, 0x4e, 0x41, 0x56, 0x41, 0x49, 0x4c, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x01, 0x2a, 0x8b, 0x01, - 0x0a, 0x0f, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x53, 0x53, - 0x45, 0x54, 0x5f, 0x52, 0x41, 0x54, 0x45, 0x53, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, - 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x10, 0x01, 0x12, 0x1a, - 0x0a, 0x16, 0x50, 0x52, 0x49, 0x43, 0x45, 0x5f, 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x51, - 0x55, 0x45, 0x52, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x4f, - 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x50, 0x49, 0x4c, 0x4f, 0x54, 0x5f, 0x45, 0x52, - 0x52, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x43, 0x43, - 0x45, 0x50, 0x54, 0x5f, 0x51, 0x55, 0x4f, 0x54, 0x45, 0x10, 0x04, 0x32, 0xd1, 0x02, 0x0a, 0x0e, - 0x50, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x50, 0x69, 0x6c, 0x6f, 0x74, 0x12, 0x65, - 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x28, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, - 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, - 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x41, - 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x72, - 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x56, - 0x65, 0x72, 0x69, 0x66, 0x79, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, - 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, - 0x66, 0x79, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x0f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x12, 0x29, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, - 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, - 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, - 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, - 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, - 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, - 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x70, - 0x63, 0x2f, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, - 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, + 0x1c, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, 0x52, 0x5f, + 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x42, 0x55, 0x59, 0x10, 0x01, 0x12, + 0x21, 0x0a, 0x1d, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x46, 0x45, + 0x52, 0x5f, 0x44, 0x49, 0x52, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x45, 0x4c, 0x4c, + 0x10, 0x02, 0x2a, 0xcd, 0x01, 0x0a, 0x06, 0x49, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x16, 0x0a, + 0x12, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, + 0x50, 0x41, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x5f, 0x48, 0x49, 0x4e, 0x54, + 0x10, 0x01, 0x12, 0x16, 0x0a, 0x12, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, + 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, 0x10, 0x02, 0x12, 0x1e, 0x0a, 0x1a, 0x49, 0x4e, + 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x50, 0x41, 0x59, 0x5f, 0x49, 0x4e, 0x56, 0x4f, 0x49, 0x43, 0x45, + 0x5f, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x59, 0x10, 0x03, 0x12, 0x1c, 0x0a, 0x18, 0x49, 0x4e, + 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, + 0x54, 0x5f, 0x48, 0x49, 0x4e, 0x54, 0x10, 0x04, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x54, 0x45, + 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x10, + 0x05, 0x12, 0x1f, 0x0a, 0x1b, 0x49, 0x4e, 0x54, 0x45, 0x4e, 0x54, 0x5f, 0x52, 0x45, 0x43, 0x56, + 0x5f, 0x50, 0x41, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x51, 0x55, 0x41, 0x4c, 0x49, 0x46, 0x59, + 0x10, 0x06, 0x2a, 0x97, 0x01, 0x0a, 0x0a, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x64, + 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x52, 0x45, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x44, 0x45, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x28, + 0x0a, 0x24, 0x52, 0x45, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, + 0x49, 0x43, 0x45, 0x5f, 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x55, 0x4e, 0x41, 0x56, 0x41, + 0x49, 0x4c, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x45, 0x4a, 0x45, + 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x4d, 0x49, 0x4e, 0x5f, 0x46, 0x49, 0x4c, 0x4c, + 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x54, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x52, 0x45, + 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x44, 0x45, 0x5f, 0x50, 0x52, 0x49, 0x43, 0x45, 0x5f, + 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4d, 0x49, 0x53, 0x53, 0x10, 0x03, 0x2a, 0xca, 0x01, 0x0a, + 0x0f, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x53, 0x53, 0x45, + 0x54, 0x5f, 0x52, 0x41, 0x54, 0x45, 0x53, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x56, + 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x10, 0x01, 0x12, 0x1a, 0x0a, + 0x16, 0x50, 0x52, 0x49, 0x43, 0x45, 0x5f, 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x51, 0x55, + 0x45, 0x52, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x4f, 0x52, + 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x50, 0x49, 0x4c, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, + 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x43, 0x43, 0x45, + 0x50, 0x54, 0x5f, 0x51, 0x55, 0x4f, 0x54, 0x45, 0x10, 0x04, 0x12, 0x14, 0x0a, 0x10, 0x4d, 0x49, + 0x4e, 0x5f, 0x46, 0x49, 0x4c, 0x4c, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x54, 0x10, 0x05, + 0x12, 0x13, 0x0a, 0x0f, 0x52, 0x41, 0x54, 0x45, 0x5f, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4d, + 0x49, 0x53, 0x53, 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x46, 0x4f, 0x4b, 0x5f, 0x4e, 0x4f, 0x54, + 0x5f, 0x56, 0x49, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x07, 0x32, 0xd1, 0x02, 0x0a, 0x0e, 0x50, 0x6f, + 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x50, 0x69, 0x6c, 0x6f, 0x74, 0x12, 0x65, 0x0a, 0x0e, + 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x28, + 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, + 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x29, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, + 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x73, + 0x6f, 0x6c, 0x76, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x6e, 0x0a, 0x11, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x41, 0x63, 0x63, + 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x2b, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, + 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, + 0x69, 0x66, 0x79, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, + 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, + 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x68, 0x0a, 0x0f, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, + 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x12, 0x29, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, + 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x2a, 0x2e, 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, + 0x6f, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x52, 0x61, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x42, 0x5a, + 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, + 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, + 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2f, + 0x70, 0x6f, 0x72, 0x74, 0x66, 0x6f, 0x6c, 0x69, 0x6f, 0x70, 0x69, 0x6c, 0x6f, 0x74, 0x72, 0x70, + 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1576,33 +1664,35 @@ var file_portfoliopilotrpc_portfolio_pilot_proto_depIdxs = []int32{ 6, // 1: portfoliopilotrpc.AssetRate.rate:type_name -> portfoliopilotrpc.FixedPoint 5, // 2: portfoliopilotrpc.BuyRequest.asset_specifier:type_name -> portfoliopilotrpc.AssetSpecifier 7, // 3: portfoliopilotrpc.BuyRequest.asset_rate_hint:type_name -> portfoliopilotrpc.AssetRate - 5, // 4: portfoliopilotrpc.SellRequest.asset_specifier:type_name -> portfoliopilotrpc.AssetSpecifier - 7, // 5: portfoliopilotrpc.SellRequest.asset_rate_hint:type_name -> portfoliopilotrpc.AssetRate - 8, // 6: portfoliopilotrpc.ResolveRequestRequest.buy_request:type_name -> portfoliopilotrpc.BuyRequest - 9, // 7: portfoliopilotrpc.ResolveRequestRequest.sell_request:type_name -> portfoliopilotrpc.SellRequest - 7, // 8: portfoliopilotrpc.ResolveRequestResponse.accept:type_name -> portfoliopilotrpc.AssetRate - 4, // 9: portfoliopilotrpc.ResolveRequestResponse.reject:type_name -> portfoliopilotrpc.RejectErr - 7, // 10: portfoliopilotrpc.AcceptedQuote.accepted_rate:type_name -> portfoliopilotrpc.AssetRate - 8, // 11: portfoliopilotrpc.AcceptedQuote.buy_request:type_name -> portfoliopilotrpc.BuyRequest - 9, // 12: portfoliopilotrpc.AcceptedQuote.sell_request:type_name -> portfoliopilotrpc.SellRequest - 12, // 13: portfoliopilotrpc.VerifyAcceptQuoteRequest.accept:type_name -> portfoliopilotrpc.AcceptedQuote - 3, // 14: portfoliopilotrpc.VerifyAcceptQuoteResponse.status:type_name -> portfoliopilotrpc.QuoteRespStatus - 5, // 15: portfoliopilotrpc.QueryAssetRatesRequest.asset_specifier:type_name -> portfoliopilotrpc.AssetSpecifier - 0, // 16: portfoliopilotrpc.QueryAssetRatesRequest.direction:type_name -> portfoliopilotrpc.AssetTransferDirection - 1, // 17: portfoliopilotrpc.QueryAssetRatesRequest.intent:type_name -> portfoliopilotrpc.Intent - 7, // 18: portfoliopilotrpc.QueryAssetRatesRequest.asset_rate_hint:type_name -> portfoliopilotrpc.AssetRate - 7, // 19: portfoliopilotrpc.QueryAssetRatesResponse.asset_rate:type_name -> portfoliopilotrpc.AssetRate - 10, // 20: portfoliopilotrpc.PortfolioPilot.ResolveRequest:input_type -> portfoliopilotrpc.ResolveRequestRequest - 13, // 21: portfoliopilotrpc.PortfolioPilot.VerifyAcceptQuote:input_type -> portfoliopilotrpc.VerifyAcceptQuoteRequest - 15, // 22: portfoliopilotrpc.PortfolioPilot.QueryAssetRates:input_type -> portfoliopilotrpc.QueryAssetRatesRequest - 11, // 23: portfoliopilotrpc.PortfolioPilot.ResolveRequest:output_type -> portfoliopilotrpc.ResolveRequestResponse - 14, // 24: portfoliopilotrpc.PortfolioPilot.VerifyAcceptQuote:output_type -> portfoliopilotrpc.VerifyAcceptQuoteResponse - 16, // 25: portfoliopilotrpc.PortfolioPilot.QueryAssetRates:output_type -> portfoliopilotrpc.QueryAssetRatesResponse - 23, // [23:26] is the sub-list for method output_type - 20, // [20:23] is the sub-list for method input_type - 20, // [20:20] is the sub-list for extension type_name - 20, // [20:20] is the sub-list for extension extendee - 0, // [0:20] is the sub-list for field type_name + 6, // 4: portfoliopilotrpc.BuyRequest.asset_rate_limit:type_name -> portfoliopilotrpc.FixedPoint + 5, // 5: portfoliopilotrpc.SellRequest.asset_specifier:type_name -> portfoliopilotrpc.AssetSpecifier + 7, // 6: portfoliopilotrpc.SellRequest.asset_rate_hint:type_name -> portfoliopilotrpc.AssetRate + 6, // 7: portfoliopilotrpc.SellRequest.asset_rate_limit:type_name -> portfoliopilotrpc.FixedPoint + 8, // 8: portfoliopilotrpc.ResolveRequestRequest.buy_request:type_name -> portfoliopilotrpc.BuyRequest + 9, // 9: portfoliopilotrpc.ResolveRequestRequest.sell_request:type_name -> portfoliopilotrpc.SellRequest + 7, // 10: portfoliopilotrpc.ResolveRequestResponse.accept:type_name -> portfoliopilotrpc.AssetRate + 4, // 11: portfoliopilotrpc.ResolveRequestResponse.reject:type_name -> portfoliopilotrpc.RejectErr + 7, // 12: portfoliopilotrpc.AcceptedQuote.accepted_rate:type_name -> portfoliopilotrpc.AssetRate + 8, // 13: portfoliopilotrpc.AcceptedQuote.buy_request:type_name -> portfoliopilotrpc.BuyRequest + 9, // 14: portfoliopilotrpc.AcceptedQuote.sell_request:type_name -> portfoliopilotrpc.SellRequest + 12, // 15: portfoliopilotrpc.VerifyAcceptQuoteRequest.accept:type_name -> portfoliopilotrpc.AcceptedQuote + 3, // 16: portfoliopilotrpc.VerifyAcceptQuoteResponse.status:type_name -> portfoliopilotrpc.QuoteRespStatus + 5, // 17: portfoliopilotrpc.QueryAssetRatesRequest.asset_specifier:type_name -> portfoliopilotrpc.AssetSpecifier + 0, // 18: portfoliopilotrpc.QueryAssetRatesRequest.direction:type_name -> portfoliopilotrpc.AssetTransferDirection + 1, // 19: portfoliopilotrpc.QueryAssetRatesRequest.intent:type_name -> portfoliopilotrpc.Intent + 7, // 20: portfoliopilotrpc.QueryAssetRatesRequest.asset_rate_hint:type_name -> portfoliopilotrpc.AssetRate + 7, // 21: portfoliopilotrpc.QueryAssetRatesResponse.asset_rate:type_name -> portfoliopilotrpc.AssetRate + 10, // 22: portfoliopilotrpc.PortfolioPilot.ResolveRequest:input_type -> portfoliopilotrpc.ResolveRequestRequest + 13, // 23: portfoliopilotrpc.PortfolioPilot.VerifyAcceptQuote:input_type -> portfoliopilotrpc.VerifyAcceptQuoteRequest + 15, // 24: portfoliopilotrpc.PortfolioPilot.QueryAssetRates:input_type -> portfoliopilotrpc.QueryAssetRatesRequest + 11, // 25: portfoliopilotrpc.PortfolioPilot.ResolveRequest:output_type -> portfoliopilotrpc.ResolveRequestResponse + 14, // 26: portfoliopilotrpc.PortfolioPilot.VerifyAcceptQuote:output_type -> portfoliopilotrpc.VerifyAcceptQuoteResponse + 16, // 27: portfoliopilotrpc.PortfolioPilot.QueryAssetRates:output_type -> portfoliopilotrpc.QueryAssetRatesResponse + 25, // [25:28] is the sub-list for method output_type + 22, // [22:25] is the sub-list for method input_type + 22, // [22:22] is the sub-list for extension type_name + 22, // [22:22] is the sub-list for extension extendee + 0, // [0:22] is the sub-list for field type_name } func init() { file_portfoliopilotrpc_portfolio_pilot_proto_init() } diff --git a/taprpc/portfoliopilotrpc/portfolio_pilot.proto b/taprpc/portfoliopilotrpc/portfolio_pilot.proto index f2184580b..4d91c7fe2 100644 --- a/taprpc/portfoliopilotrpc/portfolio_pilot.proto +++ b/taprpc/portfoliopilotrpc/portfolio_pilot.proto @@ -107,6 +107,14 @@ enum RejectCode { // REJECT_CODE_PRICE_ORACLE_UNAVAILABLE indicates that pricing could not be // provided due to an unavailable oracle. REJECT_CODE_PRICE_ORACLE_UNAVAILABLE = 1; + + // REJECT_CODE_MIN_FILL_NOT_MET indicates that the minimum fill + // constraint was not satisfiable at the accepted rate. + REJECT_CODE_MIN_FILL_NOT_MET = 2; + + // REJECT_CODE_PRICE_BOUND_MISS indicates that the accepted rate + // violated the requester's rate limit constraint. + REJECT_CODE_PRICE_BOUND_MISS = 3; } // QuoteRespStatus is an enum that represents the status of a quote response. @@ -130,6 +138,18 @@ enum QuoteRespStatus { // VALID_ACCEPT_QUOTE indicates that the accepted quote passed all // validation checks successfully. VALID_ACCEPT_QUOTE = 4; + + // MIN_FILL_NOT_MET indicates that the minimum fill constraint was + // not satisfiable at the accepted rate. + MIN_FILL_NOT_MET = 5; + + // RATE_BOUND_MISS indicates that the accepted rate violated the + // requester's rate limit constraint. + RATE_BOUND_MISS = 6; + + // FOK_NOT_VIABLE indicates that the FOK execution policy could + // not be satisfied at the accepted rate. + FOK_NOT_VIABLE = 7; } // RejectErr captures a rejection reason for a quote request. @@ -220,6 +240,14 @@ message BuyRequest { // peer_id is the 33-byte public key of the counterparty peer. bytes peer_id = 5; + + // asset_min_amount is an optional minimum asset amount. A value of 0 + // means unset. + uint64 asset_min_amount = 6; + + // asset_rate_limit is an optional minimum acceptable rate (asset units + // per BTC). + FixedPoint asset_rate_limit = 7; } // SellRequest represents a request to sell the subject asset. @@ -239,6 +267,14 @@ message SellRequest { // peer_id is the 33-byte public key of the counterparty peer. bytes peer_id = 5; + + // payment_min_amount is an optional minimum msat amount. A value of 0 + // means unset. + uint64 payment_min_amount = 6; + + // asset_rate_limit is an optional maximum acceptable rate (asset units + // per BTC). + FixedPoint asset_rate_limit = 7; } // ResolveRequestRequest specifies a quote request to resolve. diff --git a/taprpc/portfoliopilotrpc/portfolio_pilot.swagger.json b/taprpc/portfoliopilotrpc/portfolio_pilot.swagger.json index 443f9ef2e..a2cc484f4 100644 --- a/taprpc/portfoliopilotrpc/portfolio_pilot.swagger.json +++ b/taprpc/portfoliopilotrpc/portfolio_pilot.swagger.json @@ -215,6 +215,15 @@ "type": "string", "format": "byte", "description": "peer_id is the 33-byte public key of the counterparty peer." + }, + "asset_min_amount": { + "type": "string", + "format": "uint64", + "description": "asset_min_amount is an optional minimum asset amount. A value of 0\nmeans unset." + }, + "asset_rate_limit": { + "$ref": "#/definitions/portfoliopilotrpcFixedPoint", + "description": "asset_rate_limit is an optional minimum acceptable rate (asset units\nper BTC)." } }, "description": "BuyRequest represents a request to buy the subject asset." @@ -311,19 +320,24 @@ "INVALID_EXPIRY", "PRICE_ORACLE_QUERY_ERR", "PORTFOLIO_PILOT_ERR", - "VALID_ACCEPT_QUOTE" + "VALID_ACCEPT_QUOTE", + "MIN_FILL_NOT_MET", + "RATE_BOUND_MISS", + "FOK_NOT_VIABLE" ], "default": "INVALID_ASSET_RATES", - "description": "QuoteRespStatus is an enum that represents the status of a quote response.\n\n - INVALID_ASSET_RATES: INVALID_ASSET_RATES indicates that at least one asset rate in the\nquote response is invalid.\n - INVALID_EXPIRY: INVALID_EXPIRY indicates that the expiry in the quote response is\ninvalid.\n - PRICE_ORACLE_QUERY_ERR: PRICE_ORACLE_QUERY_ERR indicates that an error occurred when querying the\nprice oracle whilst evaluating the quote response.\n - PORTFOLIO_PILOT_ERR: PORTFOLIO_PILOT_ERR indicates that an unexpected error occurred in the\nportfolio pilot while evaluating the quote response.\n - VALID_ACCEPT_QUOTE: VALID_ACCEPT_QUOTE indicates that the accepted quote passed all\nvalidation checks successfully." + "description": "QuoteRespStatus is an enum that represents the status of a quote response.\n\n - INVALID_ASSET_RATES: INVALID_ASSET_RATES indicates that at least one asset rate in the\nquote response is invalid.\n - INVALID_EXPIRY: INVALID_EXPIRY indicates that the expiry in the quote response is\ninvalid.\n - PRICE_ORACLE_QUERY_ERR: PRICE_ORACLE_QUERY_ERR indicates that an error occurred when querying the\nprice oracle whilst evaluating the quote response.\n - PORTFOLIO_PILOT_ERR: PORTFOLIO_PILOT_ERR indicates that an unexpected error occurred in the\nportfolio pilot while evaluating the quote response.\n - VALID_ACCEPT_QUOTE: VALID_ACCEPT_QUOTE indicates that the accepted quote passed all\nvalidation checks successfully.\n - MIN_FILL_NOT_MET: MIN_FILL_NOT_MET indicates that the minimum fill constraint was\nnot satisfiable at the accepted rate.\n - RATE_BOUND_MISS: RATE_BOUND_MISS indicates that the accepted rate violated the\nrequester's rate limit constraint.\n - FOK_NOT_VIABLE: FOK_NOT_VIABLE indicates that the FOK execution policy could\nnot be satisfied at the accepted rate." }, "portfoliopilotrpcRejectCode": { "type": "string", "enum": [ "REJECT_CODE_UNSPECIFIED", - "REJECT_CODE_PRICE_ORACLE_UNAVAILABLE" + "REJECT_CODE_PRICE_ORACLE_UNAVAILABLE", + "REJECT_CODE_MIN_FILL_NOT_MET", + "REJECT_CODE_PRICE_BOUND_MISS" ], "default": "REJECT_CODE_UNSPECIFIED", - "description": "RejectCode represents the possible error codes that can be returned in a\nResolveRequestResponse reject result.\n\n - REJECT_CODE_UNSPECIFIED: REJECT_CODE_UNSPECIFIED indicates an unspecified error.\n - REJECT_CODE_PRICE_ORACLE_UNAVAILABLE: REJECT_CODE_PRICE_ORACLE_UNAVAILABLE indicates that pricing could not be\nprovided due to an unavailable oracle." + "description": "RejectCode represents the possible error codes that can be returned in a\nResolveRequestResponse reject result.\n\n - REJECT_CODE_UNSPECIFIED: REJECT_CODE_UNSPECIFIED indicates an unspecified error.\n - REJECT_CODE_PRICE_ORACLE_UNAVAILABLE: REJECT_CODE_PRICE_ORACLE_UNAVAILABLE indicates that pricing could not be\nprovided due to an unavailable oracle.\n - REJECT_CODE_MIN_FILL_NOT_MET: REJECT_CODE_MIN_FILL_NOT_MET indicates that the minimum fill\nconstraint was not satisfiable at the accepted rate.\n - REJECT_CODE_PRICE_BOUND_MISS: REJECT_CODE_PRICE_BOUND_MISS indicates that the accepted rate\nviolated the requester's rate limit constraint." }, "portfoliopilotrpcRejectErr": { "type": "object", @@ -391,6 +405,15 @@ "type": "string", "format": "byte", "description": "peer_id is the 33-byte public key of the counterparty peer." + }, + "payment_min_amount": { + "type": "string", + "format": "uint64", + "description": "payment_min_amount is an optional minimum msat amount. A value of 0\nmeans unset." + }, + "asset_rate_limit": { + "$ref": "#/definitions/portfoliopilotrpcFixedPoint", + "description": "asset_rate_limit is an optional maximum acceptable rate (asset units\nper BTC)." } }, "description": "SellRequest represents a request to sell the subject asset." diff --git a/taprpc/rfqrpc/rfq.pb.go b/taprpc/rfqrpc/rfq.pb.go index f6590578e..20da0d8cc 100644 --- a/taprpc/rfqrpc/rfq.pb.go +++ b/taprpc/rfqrpc/rfq.pb.go @@ -20,6 +20,57 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +// ExecutionPolicy specifies how a quote request should be filled. +type ExecutionPolicy int32 + +const ( + // EXECUTION_POLICY_IOC is Immediate-Or-Cancel: accept any partial + // fill at or above the minimum threshold. This is the default. + ExecutionPolicy_EXECUTION_POLICY_IOC ExecutionPolicy = 0 + // EXECUTION_POLICY_FOK is Fill-Or-Kill: the accepted rate must + // support the full maximum amount or the quote is rejected. + ExecutionPolicy_EXECUTION_POLICY_FOK ExecutionPolicy = 1 +) + +// Enum value maps for ExecutionPolicy. +var ( + ExecutionPolicy_name = map[int32]string{ + 0: "EXECUTION_POLICY_IOC", + 1: "EXECUTION_POLICY_FOK", + } + ExecutionPolicy_value = map[string]int32{ + "EXECUTION_POLICY_IOC": 0, + "EXECUTION_POLICY_FOK": 1, + } +) + +func (x ExecutionPolicy) Enum() *ExecutionPolicy { + p := new(ExecutionPolicy) + *p = x + return p +} + +func (x ExecutionPolicy) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ExecutionPolicy) Descriptor() protoreflect.EnumDescriptor { + return file_rfqrpc_rfq_proto_enumTypes[0].Descriptor() +} + +func (ExecutionPolicy) Type() protoreflect.EnumType { + return &file_rfqrpc_rfq_proto_enumTypes[0] +} + +func (x ExecutionPolicy) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ExecutionPolicy.Descriptor instead. +func (ExecutionPolicy) EnumDescriptor() ([]byte, []int) { + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{0} +} + // QuoteRespStatus is an enum that represents the status of a quote response. type QuoteRespStatus int32 @@ -39,6 +90,15 @@ const ( // VALID_ACCEPT_QUOTE indicates that the accepted quote passed all // validation checks successfully. QuoteRespStatus_VALID_ACCEPT_QUOTE QuoteRespStatus = 4 + // MIN_FILL_NOT_MET indicates that the minimum fill constraint was + // not satisfiable at the accepted rate. + QuoteRespStatus_MIN_FILL_NOT_MET QuoteRespStatus = 5 + // RATE_BOUND_MISS indicates that the accepted rate violated the + // requester's rate limit constraint. + QuoteRespStatus_RATE_BOUND_MISS QuoteRespStatus = 6 + // FOK_NOT_VIABLE indicates that the FOK execution policy could + // not be satisfied at the accepted rate. + QuoteRespStatus_FOK_NOT_VIABLE QuoteRespStatus = 7 ) // Enum value maps for QuoteRespStatus. @@ -49,6 +109,9 @@ var ( 2: "PRICE_ORACLE_QUERY_ERR", 3: "PORTFOLIO_PILOT_ERR", 4: "VALID_ACCEPT_QUOTE", + 5: "MIN_FILL_NOT_MET", + 6: "RATE_BOUND_MISS", + 7: "FOK_NOT_VIABLE", } QuoteRespStatus_value = map[string]int32{ "INVALID_ASSET_RATES": 0, @@ -56,6 +119,9 @@ var ( "PRICE_ORACLE_QUERY_ERR": 2, "PORTFOLIO_PILOT_ERR": 3, "VALID_ACCEPT_QUOTE": 4, + "MIN_FILL_NOT_MET": 5, + "RATE_BOUND_MISS": 6, + "FOK_NOT_VIABLE": 7, } ) @@ -70,11 +136,11 @@ func (x QuoteRespStatus) String() string { } func (QuoteRespStatus) Descriptor() protoreflect.EnumDescriptor { - return file_rfqrpc_rfq_proto_enumTypes[0].Descriptor() + return file_rfqrpc_rfq_proto_enumTypes[1].Descriptor() } func (QuoteRespStatus) Type() protoreflect.EnumType { - return &file_rfqrpc_rfq_proto_enumTypes[0] + return &file_rfqrpc_rfq_proto_enumTypes[1] } func (x QuoteRespStatus) Number() protoreflect.EnumNumber { @@ -83,7 +149,7 @@ func (x QuoteRespStatus) Number() protoreflect.EnumNumber { // Deprecated: Use QuoteRespStatus.Descriptor instead. func (QuoteRespStatus) EnumDescriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{0} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{1} } // RfqPolicyType indicates the type of policy of an RFQ session. @@ -119,11 +185,11 @@ func (x RfqPolicyType) String() string { } func (RfqPolicyType) Descriptor() protoreflect.EnumDescriptor { - return file_rfqrpc_rfq_proto_enumTypes[1].Descriptor() + return file_rfqrpc_rfq_proto_enumTypes[2].Descriptor() } func (RfqPolicyType) Type() protoreflect.EnumType { - return &file_rfqrpc_rfq_proto_enumTypes[1] + return &file_rfqrpc_rfq_proto_enumTypes[2] } func (x RfqPolicyType) Number() protoreflect.EnumNumber { @@ -132,7 +198,7 @@ func (x RfqPolicyType) Number() protoreflect.EnumNumber { // Deprecated: Use RfqPolicyType.Descriptor instead. func (RfqPolicyType) EnumDescriptor() ([]byte, []int) { - return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{1} + return file_rfqrpc_rfq_proto_rawDescGZIP(), []int{2} } type AssetSpecifier struct { @@ -362,6 +428,18 @@ type AddAssetBuyOrderRequest struct { // This field is optional and can be left empty if no metadata is available. // The maximum length of this field is 32'768 bytes. PriceOracleMetadata string `protobuf:"bytes,7,opt,name=price_oracle_metadata,json=priceOracleMetadata,proto3" json:"price_oracle_metadata,omitempty"` + // The optional minimum amount of the asset that the provider must be + // willing to offer. If set, must be less than or equal to asset_max_amt. + // A value of 0 means unset. + AssetMinAmt uint64 `protobuf:"varint,8,opt,name=asset_min_amt,json=assetMinAmt,proto3" json:"asset_min_amt,omitempty"` + // An optional rate limit constraint expressed as a fixed-point number. + // For buy orders this is the minimum acceptable rate (asset units per + // BTC). If unset, no rate floor is enforced. + AssetRateLimit *FixedPoint `protobuf:"bytes,9,opt,name=asset_rate_limit,json=assetRateLimit,proto3" json:"asset_rate_limit,omitempty"` + // The execution policy for this order. IOC (default) accepts any + // partial fill >= min threshold. FOK requires the rate to support + // the full max amount. + ExecutionPolicy ExecutionPolicy `protobuf:"varint,10,opt,name=execution_policy,json=executionPolicy,proto3,enum=rfqrpc.ExecutionPolicy" json:"execution_policy,omitempty"` } func (x *AddAssetBuyOrderRequest) Reset() { @@ -445,6 +523,27 @@ func (x *AddAssetBuyOrderRequest) GetPriceOracleMetadata() string { return "" } +func (x *AddAssetBuyOrderRequest) GetAssetMinAmt() uint64 { + if x != nil { + return x.AssetMinAmt + } + return 0 +} + +func (x *AddAssetBuyOrderRequest) GetAssetRateLimit() *FixedPoint { + if x != nil { + return x.AssetRateLimit + } + return nil +} + +func (x *AddAssetBuyOrderRequest) GetExecutionPolicy() ExecutionPolicy { + if x != nil { + return x.ExecutionPolicy + } + return ExecutionPolicy_EXECUTION_POLICY_IOC +} + type AddAssetBuyOrderResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -577,6 +676,18 @@ type AddAssetSellOrderRequest struct { // This field is optional and can be left empty if no metadata is available. // The maximum length of this field is 32'768 bytes. PriceOracleMetadata string `protobuf:"bytes,7,opt,name=price_oracle_metadata,json=priceOracleMetadata,proto3" json:"price_oracle_metadata,omitempty"` + // The optional minimum msat amount that the responding peer must agree + // to pay. If set, must be less than or equal to payment_max_amt. + // A value of 0 means unset (units: millisats). + PaymentMinAmt uint64 `protobuf:"varint,8,opt,name=payment_min_amt,json=paymentMinAmt,proto3" json:"payment_min_amt,omitempty"` + // An optional rate limit constraint expressed as a fixed-point number. + // For sell orders this is the maximum acceptable rate (asset units per + // BTC). If unset, no rate ceiling is enforced. + AssetRateLimit *FixedPoint `protobuf:"bytes,9,opt,name=asset_rate_limit,json=assetRateLimit,proto3" json:"asset_rate_limit,omitempty"` + // The execution policy for this order. IOC (default) accepts any + // partial fill >= min threshold. FOK requires the rate to support + // the full max amount. + ExecutionPolicy ExecutionPolicy `protobuf:"varint,10,opt,name=execution_policy,json=executionPolicy,proto3,enum=rfqrpc.ExecutionPolicy" json:"execution_policy,omitempty"` } func (x *AddAssetSellOrderRequest) Reset() { @@ -660,6 +771,27 @@ func (x *AddAssetSellOrderRequest) GetPriceOracleMetadata() string { return "" } +func (x *AddAssetSellOrderRequest) GetPaymentMinAmt() uint64 { + if x != nil { + return x.PaymentMinAmt + } + return 0 +} + +func (x *AddAssetSellOrderRequest) GetAssetRateLimit() *FixedPoint { + if x != nil { + return x.AssetRateLimit + } + return nil +} + +func (x *AddAssetSellOrderRequest) GetExecutionPolicy() ExecutionPolicy { + if x != nil { + return x.ExecutionPolicy + } + return ExecutionPolicy_EXECUTION_POLICY_IOC +} + type AddAssetSellOrderResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -2174,7 +2306,7 @@ var file_rfqrpc_rfq_proto_rawDesc = []byte{ 0x69, 0x63, 0x69, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x65, 0x66, 0x66, 0x69, 0x63, 0x69, 0x65, 0x6e, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x05, 0x73, 0x63, 0x61, 0x6c, 0x65, 0x22, - 0xce, 0x02, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, + 0xf4, 0x03, 0x0a, 0x17, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, @@ -2195,44 +2327,65 @@ var file_rfqrpc_rfq_proto_rawDesc = []byte{ 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x22, 0xfa, 0x01, 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, - 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, - 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, - 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, - 0x6f, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, - 0x75, 0x6f, 0x74, 0x65, 0x12, 0x43, 0x0a, 0x0d, 0x69, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, - 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, - 0x71, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0c, 0x69, 0x6e, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x46, 0x0a, 0x0e, 0x72, 0x65, 0x6a, - 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x65, 0x6a, 0x65, 0x63, - 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, - 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xd3, 0x02, - 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, - 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3f, 0x0a, 0x0f, 0x61, 0x73, - 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, - 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x0e, 0x61, 0x73, 0x73, - 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x70, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x78, - 0x41, 0x6d, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x04, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x20, 0x0a, 0x0c, 0x70, - 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x27, 0x0a, - 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, - 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x37, 0x0a, 0x18, 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x61, - 0x73, 0x73, 0x65, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x63, 0x68, 0x65, - 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, 0x73, 0x6b, 0x69, 0x70, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, - 0x32, 0x0a, 0x15, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x5f, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, - 0x70, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x22, 0xfc, 0x01, 0x0a, 0x19, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x12, 0x22, 0x0a, 0x0d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x61, 0x6d, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x61, 0x73, 0x73, 0x65, 0x74, 0x4d, 0x69, + 0x6e, 0x41, 0x6d, 0x74, 0x12, 0x3c, 0x0a, 0x10, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, + 0x74, 0x65, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x50, 0x6f, 0x69, + 0x6e, 0x74, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, + 0x69, 0x74, 0x12, 0x42, 0x0a, 0x10, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x70, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x72, + 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x52, 0x0f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, + 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x22, 0xfa, 0x01, 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, + 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, + 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, + 0x64, 0x42, 0x75, 0x79, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x61, 0x63, 0x63, + 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, 0x43, 0x0a, 0x0d, 0x69, 0x6e, + 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1c, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, + 0x00, 0x52, 0x0c, 0x69, 0x6e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x12, + 0x46, 0x0a, 0x0e, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x5f, 0x71, 0x75, 0x6f, 0x74, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, + 0x2e, 0x52, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x0d, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, + 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0xfd, 0x03, 0x0a, 0x18, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x3f, 0x0a, 0x0f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x72, 0x66, 0x71, 0x72, + 0x70, 0x63, 0x2e, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x52, 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x61, 0x78, + 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x4d, 0x61, 0x78, 0x41, 0x6d, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x78, 0x70, + 0x69, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x65, 0x78, 0x70, 0x69, 0x72, + 0x79, 0x12, 0x20, 0x0a, 0x0c, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, + 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a, 0x70, 0x65, 0x65, 0x72, 0x50, 0x75, 0x62, + 0x4b, 0x65, 0x79, 0x12, 0x27, 0x0a, 0x0f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x73, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x74, 0x69, + 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x37, 0x0a, 0x18, + 0x73, 0x6b, 0x69, 0x70, 0x5f, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, + 0x65, 0x6c, 0x5f, 0x63, 0x68, 0x65, 0x63, 0x6b, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x15, + 0x73, 0x6b, 0x69, 0x70, 0x41, 0x73, 0x73, 0x65, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x43, 0x68, 0x65, 0x63, 0x6b, 0x12, 0x32, 0x0a, 0x15, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x6f, + 0x72, 0x61, 0x63, 0x6c, 0x65, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x13, 0x70, 0x72, 0x69, 0x63, 0x65, 0x4f, 0x72, 0x61, 0x63, 0x6c, + 0x65, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x26, 0x0a, 0x0f, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x69, 0x6e, 0x5f, 0x61, 0x6d, 0x74, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x04, 0x52, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x4d, 0x69, 0x6e, 0x41, 0x6d, + 0x74, 0x12, 0x3c, 0x0a, 0x10, 0x61, 0x73, 0x73, 0x65, 0x74, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, + 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x66, + 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, + 0x0e, 0x61, 0x73, 0x73, 0x65, 0x74, 0x52, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x6d, 0x69, 0x74, 0x12, + 0x42, 0x0a, 0x10, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x70, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x72, 0x66, 0x71, 0x72, + 0x70, 0x63, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x52, 0x0f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x22, 0xfc, 0x01, 0x0a, 0x19, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x71, 0x75, 0x6f, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x72, 0x66, 0x71, 0x72, @@ -2436,65 +2589,73 @@ var file_rfqrpc_rfq_proto_rawDesc = []byte{ 0x63, 0x52, 0x09, 0x61, 0x73, 0x73, 0x65, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, 0x26, 0x0a, 0x04, 0x72, 0x61, 0x74, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x50, 0x6f, 0x69, 0x6e, 0x74, 0x52, 0x04, - 0x72, 0x61, 0x74, 0x65, 0x2a, 0x8b, 0x01, 0x0a, 0x0f, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x56, 0x41, - 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x53, 0x53, 0x45, 0x54, 0x5f, 0x52, 0x41, 0x54, 0x45, 0x53, 0x10, - 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x45, 0x58, 0x50, - 0x49, 0x52, 0x59, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, 0x49, 0x43, 0x45, 0x5f, 0x4f, - 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x51, 0x55, 0x45, 0x52, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x10, - 0x02, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x4f, 0x52, 0x54, 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x50, - 0x49, 0x4c, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x10, 0x03, 0x12, 0x16, 0x0a, 0x12, 0x56, 0x41, - 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, 0x54, 0x5f, 0x51, 0x55, 0x4f, 0x54, 0x45, - 0x10, 0x04, 0x2a, 0x47, 0x0a, 0x0d, 0x52, 0x66, 0x71, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x46, 0x51, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, - 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x41, 0x4c, 0x45, 0x10, 0x00, 0x12, 0x1c, 0x0a, - 0x18, 0x52, 0x46, 0x51, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x50, 0x55, 0x52, 0x43, 0x48, 0x41, 0x53, 0x45, 0x10, 0x01, 0x32, 0x82, 0x05, 0x0a, 0x03, - 0x52, 0x66, 0x71, 0x12, 0x55, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, - 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, - 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, - 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, - 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, + 0x72, 0x61, 0x74, 0x65, 0x2a, 0x45, 0x0a, 0x0f, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, + 0x6e, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x12, 0x18, 0x0a, 0x14, 0x45, 0x58, 0x45, 0x43, 0x55, + 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x49, 0x4f, 0x43, 0x10, + 0x00, 0x12, 0x18, 0x0a, 0x14, 0x45, 0x58, 0x45, 0x43, 0x55, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x50, + 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x46, 0x4f, 0x4b, 0x10, 0x01, 0x2a, 0xca, 0x01, 0x0a, 0x0f, + 0x51, 0x75, 0x6f, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x17, 0x0a, 0x13, 0x49, 0x4e, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x53, 0x53, 0x45, 0x54, + 0x5f, 0x52, 0x41, 0x54, 0x45, 0x53, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x49, 0x4e, 0x56, 0x41, + 0x4c, 0x49, 0x44, 0x5f, 0x45, 0x58, 0x50, 0x49, 0x52, 0x59, 0x10, 0x01, 0x12, 0x1a, 0x0a, 0x16, + 0x50, 0x52, 0x49, 0x43, 0x45, 0x5f, 0x4f, 0x52, 0x41, 0x43, 0x4c, 0x45, 0x5f, 0x51, 0x55, 0x45, + 0x52, 0x59, 0x5f, 0x45, 0x52, 0x52, 0x10, 0x02, 0x12, 0x17, 0x0a, 0x13, 0x50, 0x4f, 0x52, 0x54, + 0x46, 0x4f, 0x4c, 0x49, 0x4f, 0x5f, 0x50, 0x49, 0x4c, 0x4f, 0x54, 0x5f, 0x45, 0x52, 0x52, 0x10, + 0x03, 0x12, 0x16, 0x0a, 0x12, 0x56, 0x41, 0x4c, 0x49, 0x44, 0x5f, 0x41, 0x43, 0x43, 0x45, 0x50, + 0x54, 0x5f, 0x51, 0x55, 0x4f, 0x54, 0x45, 0x10, 0x04, 0x12, 0x14, 0x0a, 0x10, 0x4d, 0x49, 0x4e, + 0x5f, 0x46, 0x49, 0x4c, 0x4c, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, 0x4d, 0x45, 0x54, 0x10, 0x05, 0x12, + 0x13, 0x0a, 0x0f, 0x52, 0x41, 0x54, 0x45, 0x5f, 0x42, 0x4f, 0x55, 0x4e, 0x44, 0x5f, 0x4d, 0x49, + 0x53, 0x53, 0x10, 0x06, 0x12, 0x12, 0x0a, 0x0e, 0x46, 0x4f, 0x4b, 0x5f, 0x4e, 0x4f, 0x54, 0x5f, + 0x56, 0x49, 0x41, 0x42, 0x4c, 0x45, 0x10, 0x07, 0x2a, 0x47, 0x0a, 0x0d, 0x52, 0x66, 0x71, 0x50, + 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x52, 0x46, 0x51, + 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x41, 0x4c, + 0x45, 0x10, 0x00, 0x12, 0x1c, 0x0a, 0x18, 0x52, 0x46, 0x51, 0x5f, 0x50, 0x4f, 0x4c, 0x49, 0x43, + 0x59, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x55, 0x52, 0x43, 0x48, 0x41, 0x53, 0x45, 0x10, + 0x01, 0x32, 0x82, 0x05, 0x0a, 0x03, 0x52, 0x66, 0x71, 0x12, 0x55, 0x0a, 0x10, 0x41, 0x64, 0x64, + 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x1f, 0x2e, + 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, + 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, + 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x42, 0x75, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x58, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, + 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, + 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, + 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, 0x41, 0x64, - 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, + 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, - 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, - 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x58, 0x0a, 0x11, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, - 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, - 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, - 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, - 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, - 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, - 0x0a, 0x10, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, - 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, - 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, - 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x17, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, - 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, - 0x12, 0x26, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, - 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, - 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, - 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x53, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, - 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x12, 0x25, 0x2e, 0x72, 0x66, - 0x71, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, - 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x52, 0x66, 0x71, 0x45, - 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x58, 0x0a, 0x11, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, - 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x20, 0x2e, 0x72, 0x66, - 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, - 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, - 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, - 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, - 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, - 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, - 0x70, 0x63, 0x2f, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x73, 0x65, 0x74, 0x53, 0x65, 0x6c, 0x6c, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x55, 0x0a, 0x10, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, + 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, 0x65, 0x72, 0x12, 0x1f, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, + 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, 0x66, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, + 0x70, 0x63, 0x2e, 0x41, 0x64, 0x64, 0x41, 0x73, 0x73, 0x65, 0x74, 0x42, 0x75, 0x79, 0x4f, 0x66, + 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6a, 0x0a, 0x17, 0x51, + 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, + 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x12, 0x26, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, + 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, + 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x27, + 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x50, 0x65, 0x65, + 0x72, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x51, 0x75, 0x6f, 0x74, 0x65, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x53, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, + 0x73, 0x12, 0x25, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, + 0x72, 0x69, 0x62, 0x65, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4e, 0x74, 0x66, 0x6e, + 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x10, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, + 0x63, 0x2e, 0x52, 0x66, 0x71, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x30, 0x01, 0x12, 0x58, 0x0a, 0x11, + 0x46, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, + 0x79, 0x12, 0x20, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, 0x77, 0x61, + 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x2e, 0x46, 0x6f, 0x72, + 0x77, 0x61, 0x72, 0x64, 0x69, 0x6e, 0x67, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x72, 0x79, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x37, 0x5a, 0x35, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6c, 0x61, + 0x62, 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x2d, 0x61, 0x73, 0x73, 0x65, 0x74, + 0x73, 0x2f, 0x74, 0x61, 0x70, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x66, 0x71, 0x72, 0x70, 0x63, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2509,84 +2670,89 @@ func file_rfqrpc_rfq_proto_rawDescGZIP() []byte { return file_rfqrpc_rfq_proto_rawDescData } -var file_rfqrpc_rfq_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_rfqrpc_rfq_proto_enumTypes = make([]protoimpl.EnumInfo, 3) var file_rfqrpc_rfq_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_rfqrpc_rfq_proto_goTypes = []any{ - (QuoteRespStatus)(0), // 0: rfqrpc.QuoteRespStatus - (RfqPolicyType)(0), // 1: rfqrpc.RfqPolicyType - (*AssetSpecifier)(nil), // 2: rfqrpc.AssetSpecifier - (*FixedPoint)(nil), // 3: rfqrpc.FixedPoint - (*AddAssetBuyOrderRequest)(nil), // 4: rfqrpc.AddAssetBuyOrderRequest - (*AddAssetBuyOrderResponse)(nil), // 5: rfqrpc.AddAssetBuyOrderResponse - (*AddAssetSellOrderRequest)(nil), // 6: rfqrpc.AddAssetSellOrderRequest - (*AddAssetSellOrderResponse)(nil), // 7: rfqrpc.AddAssetSellOrderResponse - (*AddAssetSellOfferRequest)(nil), // 8: rfqrpc.AddAssetSellOfferRequest - (*AddAssetSellOfferResponse)(nil), // 9: rfqrpc.AddAssetSellOfferResponse - (*AddAssetBuyOfferRequest)(nil), // 10: rfqrpc.AddAssetBuyOfferRequest - (*AddAssetBuyOfferResponse)(nil), // 11: rfqrpc.AddAssetBuyOfferResponse - (*QueryPeerAcceptedQuotesRequest)(nil), // 12: rfqrpc.QueryPeerAcceptedQuotesRequest - (*AssetSpec)(nil), // 13: rfqrpc.AssetSpec - (*PeerAcceptedBuyQuote)(nil), // 14: rfqrpc.PeerAcceptedBuyQuote - (*PeerAcceptedSellQuote)(nil), // 15: rfqrpc.PeerAcceptedSellQuote - (*InvalidQuoteResponse)(nil), // 16: rfqrpc.InvalidQuoteResponse - (*RejectedQuoteResponse)(nil), // 17: rfqrpc.RejectedQuoteResponse - (*QueryPeerAcceptedQuotesResponse)(nil), // 18: rfqrpc.QueryPeerAcceptedQuotesResponse - (*SubscribeRfqEventNtfnsRequest)(nil), // 19: rfqrpc.SubscribeRfqEventNtfnsRequest - (*PeerAcceptedBuyQuoteEvent)(nil), // 20: rfqrpc.PeerAcceptedBuyQuoteEvent - (*PeerAcceptedSellQuoteEvent)(nil), // 21: rfqrpc.PeerAcceptedSellQuoteEvent - (*AcceptHtlcEvent)(nil), // 22: rfqrpc.AcceptHtlcEvent - (*RfqEvent)(nil), // 23: rfqrpc.RfqEvent - (*ForwardingHistoryRequest)(nil), // 24: rfqrpc.ForwardingHistoryRequest - (*ForwardingHistoryResponse)(nil), // 25: rfqrpc.ForwardingHistoryResponse - (*ForwardingEvent)(nil), // 26: rfqrpc.ForwardingEvent + (ExecutionPolicy)(0), // 0: rfqrpc.ExecutionPolicy + (QuoteRespStatus)(0), // 1: rfqrpc.QuoteRespStatus + (RfqPolicyType)(0), // 2: rfqrpc.RfqPolicyType + (*AssetSpecifier)(nil), // 3: rfqrpc.AssetSpecifier + (*FixedPoint)(nil), // 4: rfqrpc.FixedPoint + (*AddAssetBuyOrderRequest)(nil), // 5: rfqrpc.AddAssetBuyOrderRequest + (*AddAssetBuyOrderResponse)(nil), // 6: rfqrpc.AddAssetBuyOrderResponse + (*AddAssetSellOrderRequest)(nil), // 7: rfqrpc.AddAssetSellOrderRequest + (*AddAssetSellOrderResponse)(nil), // 8: rfqrpc.AddAssetSellOrderResponse + (*AddAssetSellOfferRequest)(nil), // 9: rfqrpc.AddAssetSellOfferRequest + (*AddAssetSellOfferResponse)(nil), // 10: rfqrpc.AddAssetSellOfferResponse + (*AddAssetBuyOfferRequest)(nil), // 11: rfqrpc.AddAssetBuyOfferRequest + (*AddAssetBuyOfferResponse)(nil), // 12: rfqrpc.AddAssetBuyOfferResponse + (*QueryPeerAcceptedQuotesRequest)(nil), // 13: rfqrpc.QueryPeerAcceptedQuotesRequest + (*AssetSpec)(nil), // 14: rfqrpc.AssetSpec + (*PeerAcceptedBuyQuote)(nil), // 15: rfqrpc.PeerAcceptedBuyQuote + (*PeerAcceptedSellQuote)(nil), // 16: rfqrpc.PeerAcceptedSellQuote + (*InvalidQuoteResponse)(nil), // 17: rfqrpc.InvalidQuoteResponse + (*RejectedQuoteResponse)(nil), // 18: rfqrpc.RejectedQuoteResponse + (*QueryPeerAcceptedQuotesResponse)(nil), // 19: rfqrpc.QueryPeerAcceptedQuotesResponse + (*SubscribeRfqEventNtfnsRequest)(nil), // 20: rfqrpc.SubscribeRfqEventNtfnsRequest + (*PeerAcceptedBuyQuoteEvent)(nil), // 21: rfqrpc.PeerAcceptedBuyQuoteEvent + (*PeerAcceptedSellQuoteEvent)(nil), // 22: rfqrpc.PeerAcceptedSellQuoteEvent + (*AcceptHtlcEvent)(nil), // 23: rfqrpc.AcceptHtlcEvent + (*RfqEvent)(nil), // 24: rfqrpc.RfqEvent + (*ForwardingHistoryRequest)(nil), // 25: rfqrpc.ForwardingHistoryRequest + (*ForwardingHistoryResponse)(nil), // 26: rfqrpc.ForwardingHistoryResponse + (*ForwardingEvent)(nil), // 27: rfqrpc.ForwardingEvent } var file_rfqrpc_rfq_proto_depIdxs = []int32{ - 2, // 0: rfqrpc.AddAssetBuyOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 14, // 1: rfqrpc.AddAssetBuyOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote - 16, // 2: rfqrpc.AddAssetBuyOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse - 17, // 3: rfqrpc.AddAssetBuyOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse - 2, // 4: rfqrpc.AddAssetSellOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 15, // 5: rfqrpc.AddAssetSellOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedSellQuote - 16, // 6: rfqrpc.AddAssetSellOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse - 17, // 7: rfqrpc.AddAssetSellOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse - 2, // 8: rfqrpc.AddAssetSellOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 2, // 9: rfqrpc.AddAssetBuyOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 3, // 10: rfqrpc.PeerAcceptedBuyQuote.ask_asset_rate:type_name -> rfqrpc.FixedPoint - 13, // 11: rfqrpc.PeerAcceptedBuyQuote.asset_spec:type_name -> rfqrpc.AssetSpec - 3, // 12: rfqrpc.PeerAcceptedSellQuote.bid_asset_rate:type_name -> rfqrpc.FixedPoint - 13, // 13: rfqrpc.PeerAcceptedSellQuote.asset_spec:type_name -> rfqrpc.AssetSpec - 0, // 14: rfqrpc.InvalidQuoteResponse.status:type_name -> rfqrpc.QuoteRespStatus - 14, // 15: rfqrpc.QueryPeerAcceptedQuotesResponse.buy_quotes:type_name -> rfqrpc.PeerAcceptedBuyQuote - 15, // 16: rfqrpc.QueryPeerAcceptedQuotesResponse.sell_quotes:type_name -> rfqrpc.PeerAcceptedSellQuote - 14, // 17: rfqrpc.PeerAcceptedBuyQuoteEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote - 15, // 18: rfqrpc.PeerAcceptedSellQuoteEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuote - 20, // 19: rfqrpc.RfqEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuoteEvent - 21, // 20: rfqrpc.RfqEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuoteEvent - 22, // 21: rfqrpc.RfqEvent.accept_htlc:type_name -> rfqrpc.AcceptHtlcEvent - 2, // 22: rfqrpc.ForwardingHistoryRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier - 26, // 23: rfqrpc.ForwardingHistoryResponse.forwards:type_name -> rfqrpc.ForwardingEvent - 1, // 24: rfqrpc.ForwardingEvent.policy_type:type_name -> rfqrpc.RfqPolicyType - 13, // 25: rfqrpc.ForwardingEvent.asset_spec:type_name -> rfqrpc.AssetSpec - 3, // 26: rfqrpc.ForwardingEvent.rate:type_name -> rfqrpc.FixedPoint - 4, // 27: rfqrpc.Rfq.AddAssetBuyOrder:input_type -> rfqrpc.AddAssetBuyOrderRequest - 6, // 28: rfqrpc.Rfq.AddAssetSellOrder:input_type -> rfqrpc.AddAssetSellOrderRequest - 8, // 29: rfqrpc.Rfq.AddAssetSellOffer:input_type -> rfqrpc.AddAssetSellOfferRequest - 10, // 30: rfqrpc.Rfq.AddAssetBuyOffer:input_type -> rfqrpc.AddAssetBuyOfferRequest - 12, // 31: rfqrpc.Rfq.QueryPeerAcceptedQuotes:input_type -> rfqrpc.QueryPeerAcceptedQuotesRequest - 19, // 32: rfqrpc.Rfq.SubscribeRfqEventNtfns:input_type -> rfqrpc.SubscribeRfqEventNtfnsRequest - 24, // 33: rfqrpc.Rfq.ForwardingHistory:input_type -> rfqrpc.ForwardingHistoryRequest - 5, // 34: rfqrpc.Rfq.AddAssetBuyOrder:output_type -> rfqrpc.AddAssetBuyOrderResponse - 7, // 35: rfqrpc.Rfq.AddAssetSellOrder:output_type -> rfqrpc.AddAssetSellOrderResponse - 9, // 36: rfqrpc.Rfq.AddAssetSellOffer:output_type -> rfqrpc.AddAssetSellOfferResponse - 11, // 37: rfqrpc.Rfq.AddAssetBuyOffer:output_type -> rfqrpc.AddAssetBuyOfferResponse - 18, // 38: rfqrpc.Rfq.QueryPeerAcceptedQuotes:output_type -> rfqrpc.QueryPeerAcceptedQuotesResponse - 23, // 39: rfqrpc.Rfq.SubscribeRfqEventNtfns:output_type -> rfqrpc.RfqEvent - 25, // 40: rfqrpc.Rfq.ForwardingHistory:output_type -> rfqrpc.ForwardingHistoryResponse - 34, // [34:41] is the sub-list for method output_type - 27, // [27:34] is the sub-list for method input_type - 27, // [27:27] is the sub-list for extension type_name - 27, // [27:27] is the sub-list for extension extendee - 0, // [0:27] is the sub-list for field type_name + 3, // 0: rfqrpc.AddAssetBuyOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 4, // 1: rfqrpc.AddAssetBuyOrderRequest.asset_rate_limit:type_name -> rfqrpc.FixedPoint + 0, // 2: rfqrpc.AddAssetBuyOrderRequest.execution_policy:type_name -> rfqrpc.ExecutionPolicy + 15, // 3: rfqrpc.AddAssetBuyOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote + 17, // 4: rfqrpc.AddAssetBuyOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse + 18, // 5: rfqrpc.AddAssetBuyOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse + 3, // 6: rfqrpc.AddAssetSellOrderRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 4, // 7: rfqrpc.AddAssetSellOrderRequest.asset_rate_limit:type_name -> rfqrpc.FixedPoint + 0, // 8: rfqrpc.AddAssetSellOrderRequest.execution_policy:type_name -> rfqrpc.ExecutionPolicy + 16, // 9: rfqrpc.AddAssetSellOrderResponse.accepted_quote:type_name -> rfqrpc.PeerAcceptedSellQuote + 17, // 10: rfqrpc.AddAssetSellOrderResponse.invalid_quote:type_name -> rfqrpc.InvalidQuoteResponse + 18, // 11: rfqrpc.AddAssetSellOrderResponse.rejected_quote:type_name -> rfqrpc.RejectedQuoteResponse + 3, // 12: rfqrpc.AddAssetSellOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 3, // 13: rfqrpc.AddAssetBuyOfferRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 4, // 14: rfqrpc.PeerAcceptedBuyQuote.ask_asset_rate:type_name -> rfqrpc.FixedPoint + 14, // 15: rfqrpc.PeerAcceptedBuyQuote.asset_spec:type_name -> rfqrpc.AssetSpec + 4, // 16: rfqrpc.PeerAcceptedSellQuote.bid_asset_rate:type_name -> rfqrpc.FixedPoint + 14, // 17: rfqrpc.PeerAcceptedSellQuote.asset_spec:type_name -> rfqrpc.AssetSpec + 1, // 18: rfqrpc.InvalidQuoteResponse.status:type_name -> rfqrpc.QuoteRespStatus + 15, // 19: rfqrpc.QueryPeerAcceptedQuotesResponse.buy_quotes:type_name -> rfqrpc.PeerAcceptedBuyQuote + 16, // 20: rfqrpc.QueryPeerAcceptedQuotesResponse.sell_quotes:type_name -> rfqrpc.PeerAcceptedSellQuote + 15, // 21: rfqrpc.PeerAcceptedBuyQuoteEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuote + 16, // 22: rfqrpc.PeerAcceptedSellQuoteEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuote + 21, // 23: rfqrpc.RfqEvent.peer_accepted_buy_quote:type_name -> rfqrpc.PeerAcceptedBuyQuoteEvent + 22, // 24: rfqrpc.RfqEvent.peer_accepted_sell_quote:type_name -> rfqrpc.PeerAcceptedSellQuoteEvent + 23, // 25: rfqrpc.RfqEvent.accept_htlc:type_name -> rfqrpc.AcceptHtlcEvent + 3, // 26: rfqrpc.ForwardingHistoryRequest.asset_specifier:type_name -> rfqrpc.AssetSpecifier + 27, // 27: rfqrpc.ForwardingHistoryResponse.forwards:type_name -> rfqrpc.ForwardingEvent + 2, // 28: rfqrpc.ForwardingEvent.policy_type:type_name -> rfqrpc.RfqPolicyType + 14, // 29: rfqrpc.ForwardingEvent.asset_spec:type_name -> rfqrpc.AssetSpec + 4, // 30: rfqrpc.ForwardingEvent.rate:type_name -> rfqrpc.FixedPoint + 5, // 31: rfqrpc.Rfq.AddAssetBuyOrder:input_type -> rfqrpc.AddAssetBuyOrderRequest + 7, // 32: rfqrpc.Rfq.AddAssetSellOrder:input_type -> rfqrpc.AddAssetSellOrderRequest + 9, // 33: rfqrpc.Rfq.AddAssetSellOffer:input_type -> rfqrpc.AddAssetSellOfferRequest + 11, // 34: rfqrpc.Rfq.AddAssetBuyOffer:input_type -> rfqrpc.AddAssetBuyOfferRequest + 13, // 35: rfqrpc.Rfq.QueryPeerAcceptedQuotes:input_type -> rfqrpc.QueryPeerAcceptedQuotesRequest + 20, // 36: rfqrpc.Rfq.SubscribeRfqEventNtfns:input_type -> rfqrpc.SubscribeRfqEventNtfnsRequest + 25, // 37: rfqrpc.Rfq.ForwardingHistory:input_type -> rfqrpc.ForwardingHistoryRequest + 6, // 38: rfqrpc.Rfq.AddAssetBuyOrder:output_type -> rfqrpc.AddAssetBuyOrderResponse + 8, // 39: rfqrpc.Rfq.AddAssetSellOrder:output_type -> rfqrpc.AddAssetSellOrderResponse + 10, // 40: rfqrpc.Rfq.AddAssetSellOffer:output_type -> rfqrpc.AddAssetSellOfferResponse + 12, // 41: rfqrpc.Rfq.AddAssetBuyOffer:output_type -> rfqrpc.AddAssetBuyOfferResponse + 19, // 42: rfqrpc.Rfq.QueryPeerAcceptedQuotes:output_type -> rfqrpc.QueryPeerAcceptedQuotesResponse + 24, // 43: rfqrpc.Rfq.SubscribeRfqEventNtfns:output_type -> rfqrpc.RfqEvent + 26, // 44: rfqrpc.Rfq.ForwardingHistory:output_type -> rfqrpc.ForwardingHistoryResponse + 38, // [38:45] is the sub-list for method output_type + 31, // [31:38] is the sub-list for method input_type + 31, // [31:31] is the sub-list for extension type_name + 31, // [31:31] is the sub-list for extension extendee + 0, // [0:31] is the sub-list for field type_name } func init() { file_rfqrpc_rfq_proto_init() } @@ -2922,7 +3088,7 @@ func file_rfqrpc_rfq_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_rfqrpc_rfq_proto_rawDesc, - NumEnums: 2, + NumEnums: 3, NumMessages: 25, NumExtensions: 0, NumServices: 1, diff --git a/taprpc/rfqrpc/rfq.proto b/taprpc/rfqrpc/rfq.proto index 4d612dd10..67b50d7de 100644 --- a/taprpc/rfqrpc/rfq.proto +++ b/taprpc/rfqrpc/rfq.proto @@ -127,6 +127,17 @@ message FixedPoint { uint32 scale = 2; } +// ExecutionPolicy specifies how a quote request should be filled. +enum ExecutionPolicy { + // EXECUTION_POLICY_IOC is Immediate-Or-Cancel: accept any partial + // fill at or above the minimum threshold. This is the default. + EXECUTION_POLICY_IOC = 0; + + // EXECUTION_POLICY_FOK is Fill-Or-Kill: the accepted rate must + // support the full maximum amount or the quote is rejected. + EXECUTION_POLICY_FOK = 1; +} + message AddAssetBuyOrderRequest { // asset_specifier is the subject asset. AssetSpecifier asset_specifier = 1; @@ -160,6 +171,21 @@ message AddAssetBuyOrderRequest { // This field is optional and can be left empty if no metadata is available. // The maximum length of this field is 32'768 bytes. string price_oracle_metadata = 7; + + // The optional minimum amount of the asset that the provider must be + // willing to offer. If set, must be less than or equal to asset_max_amt. + // A value of 0 means unset. + uint64 asset_min_amt = 8; + + // An optional rate limit constraint expressed as a fixed-point number. + // For buy orders this is the minimum acceptable rate (asset units per + // BTC). If unset, no rate floor is enforced. + FixedPoint asset_rate_limit = 9; + + // The execution policy for this order. IOC (default) accepts any + // partial fill >= min threshold. FOK requires the rate to support + // the full max amount. + ExecutionPolicy execution_policy = 10; } message AddAssetBuyOrderResponse { @@ -211,6 +237,21 @@ message AddAssetSellOrderRequest { // This field is optional and can be left empty if no metadata is available. // The maximum length of this field is 32'768 bytes. string price_oracle_metadata = 7; + + // The optional minimum msat amount that the responding peer must agree + // to pay. If set, must be less than or equal to payment_max_amt. + // A value of 0 means unset (units: millisats). + uint64 payment_min_amt = 8; + + // An optional rate limit constraint expressed as a fixed-point number. + // For sell orders this is the maximum acceptable rate (asset units per + // BTC). If unset, no rate ceiling is enforced. + FixedPoint asset_rate_limit = 9; + + // The execution policy for this order. IOC (default) accepts any + // partial fill >= min threshold. FOK requires the rate to support + // the full max amount. + ExecutionPolicy execution_policy = 10; } message AddAssetSellOrderResponse { @@ -365,6 +406,18 @@ enum QuoteRespStatus { // VALID_ACCEPT_QUOTE indicates that the accepted quote passed all // validation checks successfully. VALID_ACCEPT_QUOTE = 4; + + // MIN_FILL_NOT_MET indicates that the minimum fill constraint was + // not satisfiable at the accepted rate. + MIN_FILL_NOT_MET = 5; + + // RATE_BOUND_MISS indicates that the accepted rate violated the + // requester's rate limit constraint. + RATE_BOUND_MISS = 6; + + // FOK_NOT_VIABLE indicates that the FOK execution policy could + // not be satisfied at the accepted rate. + FOK_NOT_VIABLE = 7; } // InvalidQuoteResponse is a message that is returned when a quote response is diff --git a/taprpc/rfqrpc/rfq.swagger.json b/taprpc/rfqrpc/rfq.swagger.json index c830910d0..377e714fd 100644 --- a/taprpc/rfqrpc/rfq.swagger.json +++ b/taprpc/rfqrpc/rfq.swagger.json @@ -584,6 +584,19 @@ "price_oracle_metadata": { "type": "string", "description": "An optional text field that can be used to provide additional metadata\nabout the buy order to the price oracle. This can include information\nabout the wallet end user that initiated the transaction, or any\nauthentication information that the price oracle can use to give out a\nmore accurate (or discount) asset rate. Though not verified or enforced\nby tapd, the suggested format for this field is a JSON string.\nThis field is optional and can be left empty if no metadata is available.\nThe maximum length of this field is 32'768 bytes." + }, + "asset_min_amt": { + "type": "string", + "format": "uint64", + "description": "The optional minimum amount of the asset that the provider must be\nwilling to offer. If set, must be less than or equal to asset_max_amt.\nA value of 0 means unset." + }, + "asset_rate_limit": { + "$ref": "#/definitions/rfqrpcFixedPoint", + "description": "An optional rate limit constraint expressed as a fixed-point number.\nFor buy orders this is the minimum acceptable rate (asset units per\nBTC). If unset, no rate floor is enforced." + }, + "execution_policy": { + "$ref": "#/definitions/rfqrpcExecutionPolicy", + "description": "The execution policy for this order. IOC (default) accepts any\npartial fill \u003e= min threshold. FOK requires the rate to support\nthe full max amount." } } }, @@ -669,6 +682,19 @@ "price_oracle_metadata": { "type": "string", "description": "An optional text field that can be used to provide additional metadata\nabout the sell order to the price oracle. This can include information\nabout the wallet end user that initiated the transaction, or any\nauthentication information that the price oracle can use to give out a\nmore accurate (or discount) asset rate. Though not verified or enforced\nby tapd, the suggested format for this field is a JSON string.\nThis field is optional and can be left empty if no metadata is available.\nThe maximum length of this field is 32'768 bytes." + }, + "payment_min_amt": { + "type": "string", + "format": "uint64", + "description": "The optional minimum msat amount that the responding peer must agree\nto pay. If set, must be less than or equal to payment_max_amt.\nA value of 0 means unset (units: millisats)." + }, + "asset_rate_limit": { + "$ref": "#/definitions/rfqrpcFixedPoint", + "description": "An optional rate limit constraint expressed as a fixed-point number.\nFor sell orders this is the maximum acceptable rate (asset units per\nBTC). If unset, no rate ceiling is enforced." + }, + "execution_policy": { + "$ref": "#/definitions/rfqrpcExecutionPolicy", + "description": "The execution policy for this order. IOC (default) accepts any\npartial fill \u003e= min threshold. FOK requires the rate to support\nthe full max amount." } } }, @@ -774,6 +800,15 @@ } } }, + "rfqrpcExecutionPolicy": { + "type": "string", + "enum": [ + "EXECUTION_POLICY_IOC", + "EXECUTION_POLICY_FOK" + ], + "default": "EXECUTION_POLICY_IOC", + "description": "ExecutionPolicy specifies how a quote request should be filled.\n\n - EXECUTION_POLICY_IOC: EXECUTION_POLICY_IOC is Immediate-Or-Cancel: accept any partial\nfill at or above the minimum threshold. This is the default.\n - EXECUTION_POLICY_FOK: EXECUTION_POLICY_FOK is Fill-Or-Kill: the accepted rate must\nsupport the full maximum amount or the quote is rejected." + }, "rfqrpcFixedPoint": { "type": "object", "properties": { @@ -1045,10 +1080,13 @@ "INVALID_EXPIRY", "PRICE_ORACLE_QUERY_ERR", "PORTFOLIO_PILOT_ERR", - "VALID_ACCEPT_QUOTE" + "VALID_ACCEPT_QUOTE", + "MIN_FILL_NOT_MET", + "RATE_BOUND_MISS", + "FOK_NOT_VIABLE" ], "default": "INVALID_ASSET_RATES", - "description": "QuoteRespStatus is an enum that represents the status of a quote response.\n\n - INVALID_ASSET_RATES: INVALID_ASSET_RATES indicates that at least one asset rate in the\nquote response is invalid.\n - INVALID_EXPIRY: INVALID_EXPIRY indicates that the expiry in the quote response is\ninvalid.\n - PRICE_ORACLE_QUERY_ERR: PRICE_ORACLE_QUERY_ERR indicates that an error occurred when querying the\nprice oracle whilst evaluating the quote response.\n - PORTFOLIO_PILOT_ERR: PORTFOLIO_PILOT_ERR indicates that an unexpected error occurred in the\nportfolio pilot while evaluating the quote response.\n - VALID_ACCEPT_QUOTE: VALID_ACCEPT_QUOTE indicates that the accepted quote passed all\nvalidation checks successfully." + "description": "QuoteRespStatus is an enum that represents the status of a quote response.\n\n - INVALID_ASSET_RATES: INVALID_ASSET_RATES indicates that at least one asset rate in the\nquote response is invalid.\n - INVALID_EXPIRY: INVALID_EXPIRY indicates that the expiry in the quote response is\ninvalid.\n - PRICE_ORACLE_QUERY_ERR: PRICE_ORACLE_QUERY_ERR indicates that an error occurred when querying the\nprice oracle whilst evaluating the quote response.\n - PORTFOLIO_PILOT_ERR: PORTFOLIO_PILOT_ERR indicates that an unexpected error occurred in the\nportfolio pilot while evaluating the quote response.\n - VALID_ACCEPT_QUOTE: VALID_ACCEPT_QUOTE indicates that the accepted quote passed all\nvalidation checks successfully.\n - MIN_FILL_NOT_MET: MIN_FILL_NOT_MET indicates that the minimum fill constraint was\nnot satisfiable at the accepted rate.\n - RATE_BOUND_MISS: RATE_BOUND_MISS indicates that the accepted rate violated the\nrequester's rate limit constraint.\n - FOK_NOT_VIABLE: FOK_NOT_VIABLE indicates that the FOK execution policy could\nnot be satisfied at the accepted rate." }, "rfqrpcRejectedQuoteResponse": { "type": "object",