mirror of
https://github.com/lightninglabs/pool.git
synced 2026-08-17 13:06:52 +02:00
1030 lines
30 KiB
Go
1030 lines
30 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
"github.com/lightninglabs/pool/auctioneer"
|
|
"github.com/lightninglabs/pool/auctioneerrpc"
|
|
"github.com/lightninglabs/pool/order"
|
|
"github.com/lightninglabs/pool/poolrpc"
|
|
"github.com/lightninglabs/pool/sidecar"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"github.com/urfave/cli"
|
|
)
|
|
|
|
const (
|
|
defaultAskMaxDuration = 2016
|
|
defaultBidMinDuration = 2016
|
|
|
|
channelTypePeerDependent = "legacy"
|
|
channelTypeScriptEnforced = "script-enforced"
|
|
channelTypeSimpleTaproot = "simple-taproot"
|
|
|
|
auctionTypeInboundLiquidity = "inbound"
|
|
auctionTypeOutboundLiquidity = "outbound"
|
|
|
|
defaultMaxBatchFeeRateSatPerVByte = 100
|
|
|
|
defaultConfirmationConstraints = 1
|
|
|
|
defaultAuctionType = auctionTypeInboundLiquidity
|
|
|
|
submitOrderDescription = `
|
|
Submit a new order in the given market. Currently, Pool supports two
|
|
types of orders: asks, and bids. In Pool, market participants buy/sell
|
|
liquidity in _units_. A unit is 100,000 satoshis.
|
|
|
|
In the *inbound* market, the bidder pays the asker a premium for a
|
|
channel with 50% to 100% of inbound liquidity. The premium amount is
|
|
calculated using the funding amount of the asker.
|
|
|
|
In the outbound market, the bidder pays the asker a premium for
|
|
a channel with outbound liquidity.
|
|
To participate in the outbound market the asker needs to create an order
|
|
with '--amt=100000' (one unit) and the '--min_chan_amt' equals to
|
|
the minimum channel size that is willing to accept.
|
|
The bidder needs to create an order with '--amt=100000' (one unit)
|
|
and '--self_chan_balance' equals to the funds that is willing to commit
|
|
in a channel. The premium amount is calculated using the funding amount
|
|
of the bidder ('--self_chan_balance').
|
|
`
|
|
)
|
|
|
|
var ordersCommands = []cli.Command{
|
|
{
|
|
Name: "orders",
|
|
Aliases: []string{"o"},
|
|
Usage: "Submit/interact with orders.",
|
|
Category: "Orders",
|
|
Subcommands: []cli.Command{
|
|
ordersListCommand,
|
|
ordersCancelCommand,
|
|
{
|
|
Name: "submit",
|
|
Aliases: []string{"s"},
|
|
Usage: "submit an order",
|
|
Description: submitOrderDescription,
|
|
Subcommands: []cli.Command{
|
|
ordersSubmitAskCommand,
|
|
ordersSubmitBidCommand,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
// baseBidFlags is the set of flags that are common to any command that may
|
|
// need to accept a bid such as the main bid submission method as when a user
|
|
// attempts to offer a sidecar ticket.
|
|
var baseBidFlags = []cli.Flag{
|
|
cli.Float64Flag{
|
|
Name: "interest_rate_percent",
|
|
Usage: "the total percent one is willing to pay or " +
|
|
"accept as yield for the specified interval",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "amt",
|
|
Usage: "the amount of inbound liquidity in satoshis " +
|
|
"to request",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "acct_key",
|
|
Usage: "the account key to use to pay the order " +
|
|
"fees with",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "lease_duration_blocks",
|
|
Usage: "the number of blocks that the " +
|
|
"liquidity should be provided for",
|
|
Value: defaultBidMinDuration,
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "min_node_tier",
|
|
Usage: "the min node tier this bid should be matched " +
|
|
"with, tier 1 nodes are considered 'good', if " +
|
|
"set to tier 0, then all nodes will be " +
|
|
"considered regardless of 'quality'",
|
|
Value: uint64(order.NodeTierDefault),
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "min_chan_amt",
|
|
Usage: "the minimum amount of satoshis that a " +
|
|
"resulting channel from this order must have",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "force",
|
|
Usage: "skip order placement confirmation",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "self_chan_balance",
|
|
Usage: "give the channel leased by this bid order an " +
|
|
"initial balance by adding additional funds " +
|
|
"from our account into the channel; can be " +
|
|
"used to create up to 50/50 balanced channels",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "unannounced_channel",
|
|
Usage: "flag used to signal that this bid is interested only " +
|
|
"in unannounced channels. If this flag is not set, " +
|
|
"the channels resulting from matching this order " +
|
|
"will be announced to the network",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "zero_conf_channel",
|
|
Usage: "flag used to signal that this bid is only interested " +
|
|
"in zero conf channels",
|
|
},
|
|
}
|
|
|
|
var sharedFlags = []cli.Flag{
|
|
cli.Uint64Flag{
|
|
Name: "max_batch_fee_rate",
|
|
Usage: "the maximum fee rate (sat/vByte) to use to for " +
|
|
"the batch transaction",
|
|
Value: defaultMaxBatchFeeRateSatPerVByte,
|
|
},
|
|
cli.StringFlag{
|
|
Name: "channel_type",
|
|
Usage: fmt.Sprintf("the type of channel resulting from the "+
|
|
"order being matched (%q, %q, %q)",
|
|
channelTypePeerDependent, channelTypeScriptEnforced,
|
|
channelTypeSimpleTaproot),
|
|
},
|
|
cli.StringFlag{
|
|
Name: "auction_type",
|
|
Usage: fmt.Sprintf("the auction market where this offer "+
|
|
"must be considered in during matching (%q, %q)",
|
|
auctionTypeInboundLiquidity,
|
|
auctionTypeOutboundLiquidity),
|
|
Value: defaultAuctionType,
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "allowed_node_id",
|
|
Usage: "the list of nodes this order is allowed to match " +
|
|
"with; if empty, the order will be able to match " +
|
|
"with any node unless not_allowed_node_id is set. " +
|
|
"Can be specified multiple times",
|
|
},
|
|
cli.StringSliceFlag{
|
|
Name: "not_allowed_node_id",
|
|
Usage: "the list of nodes this order is not allowed to match " +
|
|
"with; if empty, the order will be able to match " +
|
|
"with any node unless allowed_node_id is set. Can be " +
|
|
"specified multiple times",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "public",
|
|
Usage: "flag used to signal that this order's details can be " +
|
|
"shared in public market places.",
|
|
},
|
|
}
|
|
|
|
// promptForConfirmation continuously prompts the user for the message until
|
|
// receiving a response of "yes" or "no" and returns their answer as a bool.
|
|
func promptForConfirmation(msg string) bool {
|
|
reader := bufio.NewReader(os.Stdin)
|
|
|
|
for {
|
|
fmt.Print(msg)
|
|
|
|
answer, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
answer = strings.ToLower(strings.TrimSpace(answer))
|
|
|
|
switch {
|
|
case answer == "yes":
|
|
return true
|
|
case answer == "no":
|
|
return false
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
|
|
// isBidOrder returns true for the Bid type.
|
|
func isBidOrder(orderType order.Type) bool {
|
|
return orderType == order.TypeBid
|
|
}
|
|
|
|
// isInboundLiquidityOrder returns true when the `auction_type` param is set
|
|
// to the inbound liquidity market.
|
|
func isInboundLiquidityOrder(ctx *cli.Context) bool {
|
|
return ctx.String("auction_type") == auctionTypeInboundLiquidity
|
|
}
|
|
|
|
// isOutboundLiquidityOrder returns true when the `auction_type` param is set
|
|
// to the outbound liquidity market.
|
|
func isOutboundLiquidityOrder(ctx *cli.Context) bool {
|
|
return ctx.String("auction_type") == auctionTypeOutboundLiquidity
|
|
}
|
|
|
|
// validateOrderAmount checks that the order amount is valid for the given
|
|
// market.
|
|
func validateOrderAmount(ctx *cli.Context, orderAmt uint64) error {
|
|
baseSupply := uint64(order.BaseSupplyUnit)
|
|
if isOutboundLiquidityOrder(ctx) && orderAmt != baseSupply {
|
|
return fmt.Errorf("the order amount must be exactly the base "+
|
|
"supply amount (%v sats)", baseSupply)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// getMinChanAmount returns the minChanAmount for an order taking into account
|
|
// the `min_chan_amt` parameter and the target market.
|
|
func getMinChanAmount(ctx *cli.Context, orderAmt uint64,
|
|
orderType order.Type) btcutil.Amount {
|
|
|
|
// If a `min_chan_amt` parameter was set by the user use its value.
|
|
if ctx.IsSet("min_chan_amt") {
|
|
return btcutil.Amount(ctx.Uint64("min_chan_amt"))
|
|
}
|
|
|
|
// If the `min_chan_amt` parameter was not set we use its default value
|
|
// depending on auction/order type and order amount.
|
|
switch {
|
|
// If the minimum channel amount flag wasn't provided, use a
|
|
// default of 10% and round to the nearest unit.
|
|
case isInboundLiquidityOrder(ctx):
|
|
minChanAmt := order.RoundToNextSupplyUnit(
|
|
btcutil.Amount(orderAmt) / 10,
|
|
).ToSatoshis()
|
|
return minChanAmt
|
|
|
|
// The outbound liquidity market only has default values for
|
|
// bid orders.
|
|
case isOutboundLiquidityOrder(ctx) && isBidOrder(orderType):
|
|
return order.BaseSupplyUnit
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// validateMinChanAmount checks that the minimum channel amount parameter
|
|
// has been properly set.
|
|
func validateMinChanAmount(ctx *cli.Context,
|
|
orderAmt, minChanAmt btcutil.Amount, orderType order.Type) error {
|
|
|
|
baseSupply := uint64(order.BaseSupplyUnit)
|
|
switch {
|
|
// minChanAmt will be internally expressed as supply units so it must
|
|
// be a multiple of the BaseSupplyUnit.
|
|
case minChanAmt%order.BaseSupplyUnit != 0:
|
|
return fmt.Errorf("minimum channel amount %v must be "+
|
|
"a multiple of %v", minChanAmt, baseSupply)
|
|
|
|
// We cannot match less than one unit.
|
|
case minChanAmt < order.BaseSupplyUnit:
|
|
return fmt.Errorf("minimum channel amount %v is less than "+
|
|
"acceptable lower bound (%v)", minChanAmt, baseSupply)
|
|
|
|
// Validation checks that only apply to the inbound liquidity market.
|
|
case isInboundLiquidityOrder(ctx):
|
|
if minChanAmt > orderAmt {
|
|
return fmt.Errorf("minimum channel amount %v is "+
|
|
"above order amount %v", minChanAmt,
|
|
orderAmt)
|
|
}
|
|
|
|
// Validation checks that only apply to the outbound liquidity market.
|
|
//
|
|
// NOTE: ask orders do not have any extra constraints in this market.
|
|
case isOutboundLiquidityOrder(ctx) && isBidOrder(orderType):
|
|
if minChanAmt != order.BaseSupplyUnit {
|
|
return fmt.Errorf("the minimum channel amount must be "+
|
|
"exactly equal to the base supply unit "+
|
|
"(%v sats))", baseSupply)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// validateOrderSelfBalance checks that the self balance amount is valid for
|
|
// the given market.
|
|
func validateOrderSelfBalance(ctx *cli.Context, bid *poolrpc.Bid) error {
|
|
bidAmt := btcutil.Amount(bid.Details.Amt)
|
|
bidUnits := order.NewSupplyFromSats(bidAmt)
|
|
|
|
switch {
|
|
case isInboundLiquidityOrder(ctx):
|
|
// Make sure the self channel balance is within reasonable
|
|
// limits (currently max the same of the total order amount)
|
|
// and that the min units matched is also set to 100% of the
|
|
// order amount.
|
|
if bid.Details.MinUnitsMatch != uint32(bidUnits) {
|
|
return fmt.Errorf("when using " +
|
|
"self_chan_balance the min_chan_amt must be " +
|
|
"set to the same value as amt")
|
|
}
|
|
|
|
case isOutboundLiquidityOrder(ctx):
|
|
if bid.SelfChanBalance < uint64(order.BaseSupplyUnit) {
|
|
return fmt.Errorf("the self balance amount must be at "+
|
|
"least the base supply amount (%v sats)",
|
|
order.BaseSupplyUnit)
|
|
}
|
|
}
|
|
|
|
// Default checks.
|
|
return order.CheckOfferParams(
|
|
order.AuctionType(bid.Details.AuctionType), bidAmt,
|
|
btcutil.Amount(bid.SelfChanBalance),
|
|
order.BaseSupplyUnit,
|
|
)
|
|
}
|
|
|
|
// parseCommonParams tries to read the common order parameters from the command
|
|
// line positional arguments and/or flags and parses them based on their
|
|
// destination data type. No formal in-depth validation is performed as the
|
|
// server will do that on the RPC level anyway.
|
|
func parseCommonParams(ctx *cli.Context, blockDuration uint32) (*poolrpc.Order,
|
|
error) {
|
|
|
|
var (
|
|
args = ctx.Args()
|
|
params = &poolrpc.Order{}
|
|
)
|
|
|
|
switch {
|
|
case ctx.IsSet("amt"):
|
|
params.Amt = ctx.Uint64("amt")
|
|
case args.Present():
|
|
amt, err := parseAmt(args.First())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to decode amount: %v",
|
|
err)
|
|
}
|
|
params.Amt = uint64(amt)
|
|
args = args.Tail()
|
|
}
|
|
err := validateOrderAmount(ctx, params.Amt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid order amount: %v", err)
|
|
}
|
|
|
|
params.TraderKey, err = parseAccountKey(ctx, args)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse acct_key: %v", err)
|
|
}
|
|
|
|
// Convert the cmd line flag from sat/vByte to sat/kw which is used
|
|
// internally.
|
|
satPerByte := ctx.Uint64("max_batch_fee_rate")
|
|
if satPerByte == 0 {
|
|
return nil, fmt.Errorf("max batch fee rate must be at " +
|
|
"least 1 sat/vByte")
|
|
}
|
|
|
|
satPerKw := chainfee.SatPerKVByte(satPerByte * 1000).FeePerKWeight()
|
|
|
|
// Because of rounding, we ensure the set rate is at least our fee
|
|
// floor.
|
|
if satPerKw < chainfee.FeePerKwFloor {
|
|
satPerKw = chainfee.FeePerKwFloor
|
|
}
|
|
|
|
params.MaxBatchFeeRateSatPerKw = uint64(satPerKw)
|
|
|
|
// We'll map the interest rate specified on the command line to our
|
|
// internal "rate_fixed" unit.
|
|
//
|
|
// rate = % / 100
|
|
// rate = rateFixed / totalParts
|
|
// rateFixed = rate * totalParts
|
|
interestPercent := ctx.Float64("interest_rate_percent")
|
|
interestRate := interestPercent / 100
|
|
rateFixedFloat := interestRate * order.FeeRateTotalParts
|
|
|
|
// We then take this rate fixed, and divide it by the number of blocks
|
|
// as the user wants this rate to be the final lump sum they pay.
|
|
rateFixed := uint32(rateFixedFloat / float64(blockDuration))
|
|
|
|
// At this point, if this value is less than 1, then we aren't able to
|
|
// express it given the current precision allowed by our fixed point.
|
|
if rateFixed < 1 {
|
|
return nil, fmt.Errorf("fixed rate of %v is too small "+
|
|
"(%v%% over %v blocks), min is 1 (%v%%)", rateFixed,
|
|
interestPercent, blockDuration,
|
|
float64(1)/order.FeeRateTotalParts)
|
|
}
|
|
|
|
params.RateFixed = rateFixed
|
|
|
|
// Determine the appropriate channel type that should be opened upon an
|
|
// order match.
|
|
channelType := ctx.String("channel_type")
|
|
switch channelType {
|
|
// No values means that the unknown type will be set, which means the
|
|
// sever will select a type based on the version of the connected node.
|
|
case "":
|
|
break
|
|
case channelTypePeerDependent:
|
|
params.ChannelType = auctioneerrpc.OrderChannelType_ORDER_CHANNEL_TYPE_PEER_DEPENDENT
|
|
|
|
case channelTypeScriptEnforced:
|
|
params.ChannelType = auctioneerrpc.OrderChannelType_ORDER_CHANNEL_TYPE_SCRIPT_ENFORCED
|
|
|
|
case channelTypeSimpleTaproot:
|
|
params.ChannelType = auctioneerrpc.OrderChannelType_ORDER_CHANNEL_TYPE_SIMPLE_TAPROOT
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown channel type %q", channelType)
|
|
}
|
|
|
|
auctionType := ctx.String("auction_type")
|
|
switch auctionType {
|
|
case auctionTypeInboundLiquidity:
|
|
params.AuctionType = auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_INBOUND_LIQUIDITY
|
|
|
|
case auctionTypeOutboundLiquidity:
|
|
params.AuctionType = auctioneerrpc.AuctionType_AUCTION_TYPE_BTC_OUTBOUND_LIQUIDITY
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unknown auction type %q", channelType)
|
|
}
|
|
|
|
// Get the list of node ids this order is allowed/not allowed to match
|
|
// with.
|
|
allowedNodeIDs, err := parseNodePubKeySlice(ctx, "allowed_node_id")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse allowed_node_id: %v",
|
|
err)
|
|
}
|
|
|
|
notAllowedNodeIDs, err := parseNodePubKeySlice(
|
|
ctx, "not_allowed_node_id",
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse "+
|
|
"not_allowed_node_id: %v", err)
|
|
}
|
|
|
|
// By default the order is able to match with all the orders unless
|
|
// one of this fields is specified. They are incompatible.
|
|
if len(allowedNodeIDs) > 0 && len(notAllowedNodeIDs) > 0 {
|
|
return nil, fmt.Errorf("allowed_node_id and " +
|
|
"not_allowed_node_id cannot be set together")
|
|
}
|
|
|
|
params.AllowedNodeIds = allowedNodeIDs
|
|
params.NotAllowedNodeIds = notAllowedNodeIDs
|
|
|
|
params.IsPublic = true
|
|
if ctx.IsSet("public") {
|
|
params.IsPublic = ctx.Bool("public")
|
|
}
|
|
|
|
return params, nil
|
|
}
|
|
|
|
// parseAccountKey tries to read the account key parameter from the command
|
|
// line positional arguments and/or flags.
|
|
func parseAccountKey(ctx *cli.Context, args cli.Args) ([]byte, error) {
|
|
var (
|
|
acctKeyStr string
|
|
)
|
|
switch {
|
|
case ctx.IsSet("acct_key"):
|
|
acctKeyStr = ctx.String("acct_key")
|
|
case args.Present():
|
|
acctKeyStr = args.First()
|
|
default:
|
|
return nil, fmt.Errorf("acct_key argument missing")
|
|
}
|
|
if len(acctKeyStr) != hex.EncodedLen(33) {
|
|
return nil, fmt.Errorf("acct_key in invalid format. " +
|
|
"must be hex encoded 33 byte public key")
|
|
}
|
|
return hex.DecodeString(acctKeyStr)
|
|
}
|
|
|
|
// parseNodePubKeySlice parses the list of node ids in the paramater matching
|
|
// the given `key`.
|
|
//
|
|
// NOTE: the parameter must contain a string slice. The strings are hex decoded
|
|
// but not parsed as a btcec.PublicKey.
|
|
func parseNodePubKeySlice(ctx *cli.Context, key string) ([][]byte, error) {
|
|
hexNodeIDs := ctx.StringSlice(key)
|
|
nodeIDs := make([][]byte, 0, len(hexNodeIDs))
|
|
for _, hexNodeID := range hexNodeIDs {
|
|
nodeID, err := hex.DecodeString(hexNodeID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid node ID %v: %v",
|
|
hexNodeID, err)
|
|
}
|
|
nodeIDs = append(nodeIDs, nodeID)
|
|
}
|
|
|
|
return nodeIDs, nil
|
|
}
|
|
|
|
var ordersSubmitAskCommand = cli.Command{
|
|
Name: "ask",
|
|
Usage: "offer channel liquidity",
|
|
ArgsUsage: "amt acct_key [--interest_rate_percent=R] " +
|
|
"[--max_batch_fee_rate=F] [--lease_duration_blocks=M]",
|
|
Description: `
|
|
Create an offer to provide inbound liquidity to an auction participant
|
|
by opening a channel to them for a certain time.`,
|
|
Flags: append([]cli.Flag{
|
|
cli.Float64Flag{
|
|
Name: "interest_rate_percent",
|
|
Usage: "the total percent one is willing to pay or " +
|
|
"accept as yield for the specified interval",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "amt",
|
|
Usage: "the amount to offer for channel creation in " +
|
|
"satoshis",
|
|
},
|
|
cli.StringFlag{
|
|
Name: "acct_key",
|
|
Usage: "the account key to use to offer " +
|
|
"liquidity from",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "lease_duration_blocks",
|
|
Usage: "the number of blocks that the " +
|
|
"liquidity should be offered for",
|
|
Value: defaultAskMaxDuration,
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "min_chan_amt",
|
|
Usage: "the minimum amount of satoshis that a " +
|
|
"resulting channel from this order must have",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "announcement_constraints",
|
|
Usage: "specifies if the liquidity must be sold in " +
|
|
"announced or unannounced channels. Set to 0 " +
|
|
"to express no preference, set to 1 for only " +
|
|
"announced channels and 2 for only " +
|
|
"unannounced ones. The default value is \"no " +
|
|
"preference\"",
|
|
},
|
|
cli.Uint64Flag{
|
|
Name: "confirmation_constraints",
|
|
Usage: "specifies if the liquidity must be sold in " +
|
|
"zero conf or confirmed channels. Set to 0 " +
|
|
"to express no preference, set to 1 for only " +
|
|
"confirmed channels and 2 for only zero conf " +
|
|
"ones. The default value is \"only confirmed\"",
|
|
Value: defaultConfirmationConstraints,
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "force",
|
|
Usage: "skip order placement confirmation",
|
|
},
|
|
}, sharedFlags...),
|
|
Action: ordersSubmitAsk,
|
|
}
|
|
|
|
func ordersSubmitAsk(ctx *cli.Context) error { // nolint: dupl
|
|
// Show help if no arguments or flags are provided.
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
_ = cli.ShowCommandHelp(ctx, "ask")
|
|
return nil
|
|
}
|
|
|
|
announcement := auctioneerrpc.ChannelAnnouncementConstraints(
|
|
ctx.Uint64("announcement_constraints"),
|
|
)
|
|
|
|
confirmations := auctioneerrpc.ChannelConfirmationConstraints(
|
|
ctx.Uint64("confirmation_constraints"),
|
|
)
|
|
|
|
ask := &poolrpc.Ask{
|
|
LeaseDurationBlocks: uint32(
|
|
ctx.Uint64("lease_duration_blocks"),
|
|
),
|
|
Version: uint32(order.VersionChannelType),
|
|
AnnouncementConstraints: announcement,
|
|
ConfirmationConstraints: confirmations,
|
|
}
|
|
|
|
params, err := parseCommonParams(ctx, ask.LeaseDurationBlocks)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse order params: %v", err)
|
|
}
|
|
|
|
minChanAmt := getMinChanAmount(ctx, params.Amt, order.TypeAsk)
|
|
err = validateMinChanAmount(
|
|
ctx, btcutil.Amount(params.Amt), minChanAmt, order.TypeAsk,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("invalid min chan amount: %v", err)
|
|
}
|
|
params.MinUnitsMatch = uint32(minChanAmt / order.BaseSupplyUnit)
|
|
|
|
ask.Details = params
|
|
|
|
client, cleanup, err := getClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
// If the user didn't opt to force submit this order, then we'll show a
|
|
// break down of the final order details and request a confirmation
|
|
// before we submit.
|
|
if !ctx.Bool("force") {
|
|
if err := printOrderDetails(
|
|
client, btcutil.Amount(ask.Details.Amt),
|
|
order.SupplyUnit(ask.Details.MinUnitsMatch),
|
|
0, order.FixedRatePremium(ask.Details.RateFixed),
|
|
ask.LeaseDurationBlocks,
|
|
chainfee.SatPerKWeight(
|
|
ask.Details.MaxBatchFeeRateSatPerKw,
|
|
), true, ask.Details.IsPublic, nil,
|
|
); err != nil {
|
|
return fmt.Errorf("unable to print order details: %v",
|
|
err)
|
|
}
|
|
|
|
if !promptForConfirmation("Confirm order (yes/no): ") {
|
|
fmt.Println("Cancelling order...")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
resp, err := client.SubmitOrder(
|
|
context.Background(), &poolrpc.SubmitOrderRequest{
|
|
Details: &poolrpc.SubmitOrderRequest_Ask{
|
|
Ask: ask,
|
|
},
|
|
Initiator: defaultInitiator,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printRespJSON(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
func printOrderDetails(client poolrpc.TraderClient, amt btcutil.Amount,
|
|
minUnitsMatch order.SupplyUnit, selfChanBalance btcutil.Amount,
|
|
rate order.FixedRatePremium, leaseDuration uint32,
|
|
maxBatchFeeRate chainfee.SatPerKWeight, isAsk, isPublic bool,
|
|
sidecarTicket *sidecar.Ticket) error {
|
|
|
|
quote, err := client.QuoteOrder(
|
|
context.Background(), &poolrpc.QuoteOrderRequest{
|
|
Amt: uint64(amt),
|
|
RateFixed: uint32(rate),
|
|
LeaseDurationBlocks: leaseDuration,
|
|
MaxBatchFeeRateSatPerKw: uint64(maxBatchFeeRate),
|
|
MinUnitsMatch: uint32(minUnitsMatch),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
orderType := "Bid"
|
|
premiumDescription := "paid to maker"
|
|
if isAsk {
|
|
orderType = "Ask"
|
|
premiumDescription = "yield from taker"
|
|
}
|
|
|
|
fmt.Println("-- Order Details --")
|
|
fmt.Printf("%v Amount: %v\n", orderType, amt)
|
|
fmt.Printf("%v Duration: %v\n", orderType, leaseDuration)
|
|
fmt.Printf("Total Premium (%v): %v \n", premiumDescription,
|
|
btcutil.Amount(quote.TotalPremiumSat))
|
|
fmt.Printf("Rate Fixed: %v\n", rate)
|
|
fmt.Printf("Rate Per Block: %.9f (%.7f%%)\n", quote.RatePerBlock,
|
|
quote.RatePercent)
|
|
fmt.Println("Execution Fee: ",
|
|
btcutil.Amount(quote.TotalExecutionFeeSat))
|
|
fmt.Printf("Max batch fee rate: %d sat/vByte\n",
|
|
maxBatchFeeRate.FeePerKVByte()/1000)
|
|
fmt.Println("Max chain fee:",
|
|
btcutil.Amount(quote.WorstCaseChainFeeSat))
|
|
|
|
if selfChanBalance > 0 {
|
|
fmt.Printf("Self channel balance: %v\n", selfChanBalance)
|
|
}
|
|
|
|
fmt.Printf("Is public: %t\n", isPublic)
|
|
|
|
if sidecarTicket != nil {
|
|
fmt.Println("Sidecar order: ")
|
|
fmt.Printf(" Recipient node: %x\n",
|
|
sidecarTicket.Recipient.NodePubKey.SerializeCompressed())
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var ordersSubmitBidCommand = cli.Command{
|
|
Name: "bid",
|
|
Usage: "obtain channel liquidity",
|
|
ArgsUsage: "amt acct_key [--interest_rate_percent=R]" +
|
|
"[--max_batch_fee_rate=F] [--lease_duration_blocks=M]",
|
|
Description: `
|
|
Place an offer for acquiring inbound liquidity by lending
|
|
funding capacity from another participant in the order book.`,
|
|
Flags: append(
|
|
append(
|
|
baseBidFlags,
|
|
cli.StringFlag{
|
|
Name: "sidecar_ticket",
|
|
Usage: "instead of leasing a channel for the node " +
|
|
"connected to this pool instance, lease a " +
|
|
"channel for another node; use the " +
|
|
"information within the ticket to identify " +
|
|
"the receiver of the sidecar channel; using " +
|
|
"a sidecar ticket will also overwrite the " +
|
|
"amt, min_chan_amt, lease_duration_blocks " +
|
|
"and self_chan_balance fields",
|
|
},
|
|
), sharedFlags...,
|
|
),
|
|
Action: ordersSubmitBid,
|
|
}
|
|
|
|
func parseBaseBid(ctx *cli.Context) (*poolrpc.Bid, *sidecar.Ticket, error) {
|
|
// The node tier values are a bit un-intuitive. We need to convert
|
|
// between the human interpretation of "tier 1" (value 1) to the
|
|
// internal representation of "tier 1" (value order.NodeTier1=2).
|
|
cliNodeTier := order.NodeTierDefault
|
|
if ctx.IsSet("min_node_tier") {
|
|
if ctx.Uint64("min_node_tier") == 0 {
|
|
cliNodeTier = order.NodeTier0
|
|
}
|
|
if ctx.Uint64("min_node_tier") == 1 {
|
|
cliNodeTier = order.NodeTier1
|
|
}
|
|
}
|
|
|
|
nodeTier, err := auctioneer.MarshallNodeTier(cliNodeTier)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
bid := &poolrpc.Bid{
|
|
LeaseDurationBlocks: uint32(
|
|
ctx.Uint64("lease_duration_blocks"),
|
|
),
|
|
Version: uint32(order.VersionChannelType),
|
|
MinNodeTier: nodeTier,
|
|
UnannouncedChannel: ctx.Bool("unannounced_channel"),
|
|
ZeroConfChannel: ctx.Bool("zero_conf_channel"),
|
|
}
|
|
|
|
// Let's find out if this is an order for a sidecar channel because if
|
|
// it is, we can take some of the information out of the ticket and
|
|
// don't require the user to enter them manually again.
|
|
var ticket *sidecar.Ticket
|
|
if ctx.IsSet("sidecar_ticket") {
|
|
// The ticket is expected in the string encoded version which
|
|
// has a prefix and a checksum. We're supposed to send it to
|
|
// the daemon in its raw format though. So let's decode and
|
|
// check it in the process.
|
|
ticket, err = sidecar.DecodeString(ctx.String("sidecar_ticket"))
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to parse sidecar "+
|
|
"ticket: %v", err)
|
|
}
|
|
|
|
// Let's make sure the ticket is in the correct state. This will
|
|
// be checked by the server as well but we want to make sure we
|
|
// don't run into a nil reference when printing the order
|
|
// details below.
|
|
if ticket.State != sidecar.StateRegistered ||
|
|
ticket.Recipient == nil {
|
|
|
|
return nil, nil, fmt.Errorf("unexpected sidecar "+
|
|
"ticket state %d, possibly not registered "+
|
|
"with recipient node yet", ticket.State)
|
|
}
|
|
|
|
// With the ticket parsed and formally checked, we can now pre-
|
|
// fill the amount and min channel amount. Those values must
|
|
// match the offered capacity, otherwise the push amount won't
|
|
// work as expected and the protocol would get more complex as
|
|
// well.
|
|
amtStr := fmt.Sprintf("%d", ticket.Offer.Capacity)
|
|
pushAmtStr := fmt.Sprintf("%d", ticket.Offer.PushAmt)
|
|
_ = ctx.Set("amt", amtStr)
|
|
_ = ctx.Set("min_chan_amt", amtStr)
|
|
_ = ctx.Set("self_chan_balance", pushAmtStr)
|
|
bid.LeaseDurationBlocks = ticket.Offer.LeaseDurationBlocks
|
|
|
|
// Looks good so far. The rest will be checked server side. For
|
|
// now we can just add the ticket to the order.
|
|
bid.SidecarTicket = ctx.String("sidecar_ticket")
|
|
}
|
|
|
|
params, err := parseCommonParams(ctx, bid.LeaseDurationBlocks)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("unable to parse order "+
|
|
"params: %v", err)
|
|
}
|
|
|
|
minChanAmt := getMinChanAmount(ctx, params.Amt, order.TypeBid)
|
|
err = validateMinChanAmount(
|
|
ctx, btcutil.Amount(params.Amt), minChanAmt, order.TypeBid,
|
|
)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid min chan amount: %v", err)
|
|
}
|
|
params.MinUnitsMatch = uint32(minChanAmt / order.BaseSupplyUnit)
|
|
|
|
bid.Details = params
|
|
|
|
hasSelfChanBalance := ctx.IsSet("self_chan_balance")
|
|
switch {
|
|
case !hasSelfChanBalance && isOutboundLiquidityOrder(ctx):
|
|
return nil, nil, fmt.Errorf("the `self_chan_balance` "+
|
|
"parameter must be set to at least %v sats",
|
|
order.BaseSupplyUnit)
|
|
|
|
case hasSelfChanBalance:
|
|
bid.SelfChanBalance = ctx.Uint64("self_chan_balance")
|
|
if err = validateOrderSelfBalance(ctx, bid); err != nil {
|
|
return nil, nil, fmt.Errorf("invalid self balance "+
|
|
"amount: %v", err)
|
|
}
|
|
}
|
|
|
|
return bid, ticket, nil
|
|
}
|
|
|
|
func ordersSubmitBid(ctx *cli.Context) error { // nolint: dupl
|
|
// Show help if no arguments or flags are provided.
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
_ = cli.ShowCommandHelp(ctx, "bid")
|
|
return nil
|
|
}
|
|
|
|
bid, ticket, err := parseBaseBid(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
client, cleanup, err := getClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
// If the user didn't opt to force submit this order, then we'll show a
|
|
// break down of the final order details and request a confirmation
|
|
// before we submit.
|
|
if !ctx.Bool("force") {
|
|
if err := printOrderDetails(
|
|
client, btcutil.Amount(bid.Details.Amt),
|
|
order.SupplyUnit(bid.Details.MinUnitsMatch),
|
|
btcutil.Amount(bid.SelfChanBalance),
|
|
order.FixedRatePremium(bid.Details.RateFixed),
|
|
bid.LeaseDurationBlocks,
|
|
chainfee.SatPerKWeight(
|
|
bid.Details.MaxBatchFeeRateSatPerKw,
|
|
), false, bid.Details.IsPublic, ticket,
|
|
); err != nil {
|
|
return fmt.Errorf("unable to print order details: %v",
|
|
err)
|
|
}
|
|
|
|
if !promptForConfirmation("Confirm order (yes/no): ") {
|
|
fmt.Println("Cancelling order...")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
resp, err := client.SubmitOrder(
|
|
context.Background(), &poolrpc.SubmitOrderRequest{
|
|
Details: &poolrpc.SubmitOrderRequest_Bid{
|
|
Bid: bid,
|
|
},
|
|
Initiator: defaultInitiator,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
|
|
return nil
|
|
}
|
|
|
|
var ordersListCommand = cli.Command{
|
|
Name: "list",
|
|
Aliases: []string{"l"},
|
|
Usage: "list all existing orders",
|
|
Description: `
|
|
List all orders that are stored in the local order database`,
|
|
Flags: []cli.Flag{
|
|
cli.BoolFlag{
|
|
Name: "verbose",
|
|
Usage: "show verbose output including events",
|
|
},
|
|
cli.BoolFlag{
|
|
Name: "show_archived",
|
|
Usage: "include orders no longer active",
|
|
},
|
|
},
|
|
Action: ordersList,
|
|
}
|
|
|
|
func ordersList(ctx *cli.Context) error {
|
|
client, cleanup, err := getClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
// Default to only showing active orders.
|
|
activeOnly := true
|
|
if ctx.Bool("show_archived") {
|
|
activeOnly = false
|
|
}
|
|
|
|
resp, err := client.ListOrders(
|
|
context.Background(), &poolrpc.ListOrdersRequest{
|
|
Verbose: ctx.Bool("verbose"),
|
|
ActiveOnly: activeOnly,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|
|
|
|
var ordersCancelCommand = cli.Command{
|
|
Name: "cancel",
|
|
Aliases: []string{"c"},
|
|
Usage: "remove an order from the order book by canceling it",
|
|
ArgsUsage: "order_nonce",
|
|
Description: `
|
|
Remove a pending offer from the order book.`,
|
|
Flags: []cli.Flag{
|
|
cli.StringFlag{
|
|
Name: "order_nonce",
|
|
Usage: "the order nonce of the order to cancel",
|
|
},
|
|
},
|
|
Action: ordersCancel,
|
|
}
|
|
|
|
func ordersCancel(ctx *cli.Context) error { // nolint: dupl
|
|
// Show help if no arguments or flags are provided.
|
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
|
_ = cli.ShowCommandHelp(ctx, "cancel")
|
|
return nil
|
|
}
|
|
|
|
var (
|
|
nonceHex string
|
|
args = ctx.Args()
|
|
)
|
|
switch {
|
|
case ctx.IsSet("order_nonce"):
|
|
nonceHex = ctx.String("order_nonce")
|
|
case args.Present():
|
|
nonceHex = args.First()
|
|
default:
|
|
return fmt.Errorf("order_nonce argument missing")
|
|
}
|
|
nonce, err := hex.DecodeString(nonceHex)
|
|
if err != nil {
|
|
return fmt.Errorf("cannot hex decode order nonce: %v", err)
|
|
}
|
|
|
|
client, cleanup, err := getClient(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
resp, err := client.CancelOrder(
|
|
context.Background(), &poolrpc.CancelOrderRequest{
|
|
OrderNonce: nonce,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printRespJSON(resp)
|
|
return nil
|
|
}
|