diff --git a/x/exchange/fulfillment.go b/x/exchange/fulfillment.go index 3358729831..c1d2b1181f 100644 --- a/x/exchange/fulfillment.go +++ b/x/exchange/fulfillment.go @@ -10,37 +10,163 @@ import ( banktypes "github.com/cosmos/cosmos-sdk/x/bank/types" ) -// OrderSplit contains an order, and the asset and price amounts that should come out of it. -// TODO[1658]: Remove this struct. -type OrderSplit struct { - // Order fulfillment associated with this split. - Order *OrderFulfillment - // Assets is the amount of assets from the order involved in this split. - Assets sdk.Coin - // Price is the amount of the price from the order involved in this split. - Price sdk.Coin +// Transfer contains bank inputs and outputs indicating a transfer that needs to be made. +type Transfer struct { + // Inputs are the inputs that make up this transfer. + Inputs []banktypes.Input + // Outputs are the outputs that make up this transfer. + Outputs []banktypes.Output +} + +// Settlement contains information on how a set of orders is to be settled. +type Settlement struct { + // Transfers are all of the inputs and outputs needed to facilitate movement of assets and price. + Transfers []*Transfer + // FeeInputs are the inputs needed to facilitate payment of order fees. + FeeInputs []banktypes.Input + // FullyFilledOrders are all the orders that are fully filled in this settlement. + // If there is an order that's being partially filled, it will not be included in this list. + FullyFilledOrders []*FilledOrder + // PartialOrderFilled is a partially filled order with amounts indicating how much was filled. + // This is not included in FullyFilledOrders. + PartialOrderFilled *FilledOrder + // PartialOrderLeft is what's left of the partially filled order. + PartialOrderLeft *Order +} + +// BuildSettlement processes the provided orders, identifying how the provided orders can be settled. +func BuildSettlement(askOrders, bidOrders []*Order, sellerFeeRatioLookup func(denom string) (*FeeRatio, error)) (*Settlement, error) { + if err := validateCanSettle(askOrders, bidOrders); err != nil { + return nil, err + } + + askOFs := newOrderFulfillments(askOrders) + bidOFs := newOrderFulfillments(bidOrders) + + // Allocate the assets first. + if err := allocateAssets(askOFs, bidOFs); err != nil { + return nil, err + } + + settlement := &Settlement{} + // Identify any partial order and update its entry in the order fulfillments. + if err := splitPartial(askOFs, bidOFs, settlement); err != nil { + return nil, err + } + + // Allocate the prices. + if err := allocatePrice(askOFs, bidOFs); err != nil { + return nil, err + } + + // Set the fees in the fulfillments + sellerFeeRatio, err := sellerFeeRatioLookup(askOFs[0].GetPrice().Denom) + if err != nil { + return nil, err + } + if err = setFeesToPay(askOFs, bidOFs, sellerFeeRatio); err != nil { + return nil, err + } + + // Make sure everything adds up. + if err = validateFulfillments(askOFs, bidOFs); err != nil { + return nil, err + } + + // Create the transfers + if err = buildTransfers(askOFs, bidOFs, settlement); err != nil { + return nil, err + } + + // Indicate what's been filled in full and partially. + populateFilled(askOFs, bidOFs, settlement) + + return settlement, nil +} + +// indexedAddrAmts is a set of addresses and amounts. +type indexedAddrAmts struct { + // addrs are a list of all addresses that have amounts. + addrs []string + // amts are a list of the coin amounts for each address (by slice index). + amts []sdk.Coins + // indexes are the index value for each address. + indexes map[string]int +} + +func newIndexedAddrAmts() *indexedAddrAmts { + return &indexedAddrAmts{ + indexes: make(map[string]int), + } +} + +// add adds the coins to the given address. +// Panics if a provided coin is invalid. +func (i *indexedAddrAmts) add(addr string, coins ...sdk.Coin) { + for _, coin := range coins { + if err := coin.Validate(); err != nil { + panic(fmt.Errorf("cannot index and add invalid coin amount %q", coin)) + } + } + n, known := i.indexes[addr] + if !known { + n = len(i.addrs) + i.indexes[addr] = n + i.addrs = append(i.addrs, addr) + i.amts = append(i.amts, sdk.NewCoins()) + } + i.amts[n] = i.amts[n].Add(coins...) +} + +// getAsInputs returns all the entries as bank Inputs. +// Panics if this is nil, has no addrs, or has a negative coin amount. +func (i *indexedAddrAmts) getAsInputs() []banktypes.Input { + if i == nil || len(i.addrs) == 0 { + return nil + } + rv := make([]banktypes.Input, len(i.addrs)) + for n, addr := range i.addrs { + if !i.amts[n].IsAllPositive() { + panic(fmt.Errorf("invalid indexed amount %q for address %q: cannot be zero or negative", addr, i.amts[n])) + } + rv[n] = banktypes.Input{Address: addr, Coins: i.amts[n]} + } + return rv +} + +// getAsOutputs returns all the entries as bank Outputs. +// Panics if this is nil, has no addrs, or has a negative coin amount. +func (i *indexedAddrAmts) getAsOutputs() []banktypes.Output { + if i == nil || len(i.addrs) == 0 { + return nil + } + rv := make([]banktypes.Output, len(i.addrs)) + for n, addr := range i.addrs { + if !i.amts[n].IsAllPositive() { + panic(fmt.Errorf("invalid indexed amount %q for address %q: cannot be zero or negative", addr, i.amts[n])) + } + rv[n] = banktypes.Output{Address: addr, Coins: i.amts[n]} + } + return rv } -// Distribution indicates an address and an amount that will either go to, or come from that address. -type Distribution struct { - // Address is an bech32 address string +// distribution indicates an address and an amount that will either go to, or come from that address. +type distribution struct { + // Address is a bech32 address string Address string // Amount is the amount that will either go to, or come from that address. Amount sdkmath.Int } -// OrderFulfillment is used to figure out how an order should be fulfilled. -type OrderFulfillment struct { +// orderFulfillment is used to figure out how an order should be fulfilled. +type orderFulfillment struct { // Order is the original order with all its information. Order *Order - // Splits contains information on the orders being used to fulfill this order. - Splits []*OrderSplit // TODO[1658]: Remove this field. - // AssetDists contains distribution info for this order's assets. - AssetDists []*Distribution + AssetDists []*distribution // AssetDists contains distribution info for this order's price. - PriceDists []*Distribution + PriceDists []*distribution // AssetsFilledAmt is the total amount of assets being fulfilled for the order. AssetsFilledAmt sdkmath.Int @@ -53,175 +179,140 @@ type OrderFulfillment struct { // PriceLeftAmt is the price that has not yet been fulfilled for the order. // This can be negative for ask orders that are being filled at a higher price than requested. PriceLeftAmt sdkmath.Int - - // IsFinalized is set to true once Finalize() is called without error. - IsFinalized bool // FeesToPay is the amount of settlement fees the order owner should pay to settle this order. - // This is only set during Finalize(). FeesToPay sdk.Coins - // OrderFeesLeft is the amount fees settlement left to pay (if this order is only partially filled). - // This is only set during Finalize(). - OrderFeesLeft sdk.Coins - // PriceFilledAmt is the amount of the order price that is being filled. - // This is only set during Finalize(). - PriceFilledAmt sdkmath.Int - // PriceUnfilledAmt is the amount of the order price that is not being filled. - // This is only set during Finalize(). - PriceUnfilledAmt sdkmath.Int } -var _ OrderI = (*OrderFulfillment)(nil) +var _ OrderI = (*orderFulfillment)(nil) -// NewOrderFulfillment creates a new OrderFulfillment wrapping the provided order. -func NewOrderFulfillment(order *Order) *OrderFulfillment { - return &OrderFulfillment{ +// newOrderFulfillment creates a new orderFulfillment wrapping the provided order. +func newOrderFulfillment(order *Order) *orderFulfillment { + return &orderFulfillment{ Order: order, AssetsFilledAmt: sdkmath.ZeroInt(), AssetsUnfilledAmt: order.GetAssets().Amount, PriceAppliedAmt: sdkmath.ZeroInt(), PriceLeftAmt: order.GetPrice().Amount, - PriceFilledAmt: sdkmath.ZeroInt(), - PriceUnfilledAmt: sdkmath.ZeroInt(), } } -// NewOrderFulfillments creates a new OrderFulfillment for each of the provided orders. -func NewOrderFulfillments(orders []*Order) []*OrderFulfillment { - rv := make([]*OrderFulfillment, len(orders)) +// newOrderFulfillments creates a new orderFulfillment for each of the provided orders. +func newOrderFulfillments(orders []*Order) []*orderFulfillment { + rv := make([]*orderFulfillment, len(orders)) for i, o := range orders { - rv[i] = NewOrderFulfillment(o) + rv[i] = newOrderFulfillment(o) } return rv } -// assetCoin returns a coin with the given amount and the same denom as this order's assets. -func (f OrderFulfillment) assetCoin(amt sdkmath.Int) sdk.Coin { +// AssetCoin returns a coin with the given amount and the same denom as this order's assets. +func (f orderFulfillment) AssetCoin(amt sdkmath.Int) sdk.Coin { return sdk.Coin{Denom: f.GetAssets().Denom, Amount: amt} } -// priceCoin returns a coin with the given amount and the same denom as this order's price. -func (f OrderFulfillment) priceCoin(amt sdkmath.Int) sdk.Coin { +// PriceCoin returns a coin with the given amount and the same denom as this order's price. +func (f orderFulfillment) PriceCoin(amt sdkmath.Int) sdk.Coin { return sdk.Coin{Denom: f.GetPrice().Denom, Amount: amt} } // GetAssetsFilled gets the coin value of the assets that have been filled in this fulfillment. -func (f OrderFulfillment) GetAssetsFilled() sdk.Coin { - return f.assetCoin(f.AssetsFilledAmt) +func (f orderFulfillment) GetAssetsFilled() sdk.Coin { + return f.AssetCoin(f.AssetsFilledAmt) } // GetAssetsUnfilled gets the coin value of the assets left to fill in this fulfillment. -func (f OrderFulfillment) GetAssetsUnfilled() sdk.Coin { - return f.assetCoin(f.AssetsUnfilledAmt) +func (f orderFulfillment) GetAssetsUnfilled() sdk.Coin { + return f.AssetCoin(f.AssetsUnfilledAmt) } // GetPriceApplied gets the coin value of the price that has been filled in this fulfillment. -func (f OrderFulfillment) GetPriceApplied() sdk.Coin { - return f.priceCoin(f.PriceAppliedAmt) +func (f orderFulfillment) GetPriceApplied() sdk.Coin { + return f.PriceCoin(f.PriceAppliedAmt) } // GetPriceLeft gets the coin value of the price left to fill in this fulfillment. -func (f OrderFulfillment) GetPriceLeft() sdk.Coin { - return f.priceCoin(f.PriceLeftAmt) -} - -// GetPriceFilled gets the coin value of the price filled in this fulfillment. -func (f OrderFulfillment) GetPriceFilled() sdk.Coin { - return f.priceCoin(f.PriceFilledAmt) -} - -// GetPriceUnfilled gets the coin value of the price unfilled in this fulfillment. -func (f OrderFulfillment) GetPriceUnfilled() sdk.Coin { - return f.priceCoin(f.PriceUnfilledAmt) -} - -// IsFullyFilled returns true if this fulfillment's order has been fully accounted for. -func (f OrderFulfillment) IsFullyFilled() bool { - return !f.AssetsUnfilledAmt.IsPositive() -} - -// IsCompletelyUnfulfilled returns true if nothing in this order has been filled. -func (f OrderFulfillment) IsCompletelyUnfulfilled() bool { - return f.AssetsFilledAmt.IsZero() +func (f orderFulfillment) GetPriceLeft() sdk.Coin { + return f.PriceCoin(f.PriceLeftAmt) } // GetOrderID gets this fulfillment's order's id. -func (f OrderFulfillment) GetOrderID() uint64 { +func (f orderFulfillment) GetOrderID() uint64 { return f.Order.GetOrderID() } // IsAskOrder returns true if this is an ask order. -func (f OrderFulfillment) IsAskOrder() bool { +func (f orderFulfillment) IsAskOrder() bool { return f.Order.IsAskOrder() } // IsBidOrder returns true if this is an ask order. -func (f OrderFulfillment) IsBidOrder() bool { +func (f orderFulfillment) IsBidOrder() bool { return f.Order.IsBidOrder() } // GetMarketID gets this fulfillment's order's market id. -func (f OrderFulfillment) GetMarketID() uint32 { +func (f orderFulfillment) GetMarketID() uint32 { return f.Order.GetMarketID() } // GetOwner gets this fulfillment's order's owner. -func (f OrderFulfillment) GetOwner() string { +func (f orderFulfillment) GetOwner() string { return f.Order.GetOwner() } // GetAssets gets this fulfillment's order's assets. -func (f OrderFulfillment) GetAssets() sdk.Coin { +func (f orderFulfillment) GetAssets() sdk.Coin { return f.Order.GetAssets() } // GetPrice gets this fulfillment's order's price. -func (f OrderFulfillment) GetPrice() sdk.Coin { +func (f orderFulfillment) GetPrice() sdk.Coin { return f.Order.GetPrice() } // GetSettlementFees gets this fulfillment's order's settlement fees. -func (f OrderFulfillment) GetSettlementFees() sdk.Coins { +func (f orderFulfillment) GetSettlementFees() sdk.Coins { return f.Order.GetSettlementFees() } // PartialFillAllowed gets this fulfillment's order's AllowPartial flag. -func (f OrderFulfillment) PartialFillAllowed() bool { +func (f orderFulfillment) PartialFillAllowed() bool { return f.Order.PartialFillAllowed() } // GetOrderType gets this fulfillment's order's type string. -func (f OrderFulfillment) GetOrderType() string { +func (f orderFulfillment) GetOrderType() string { return f.Order.GetOrderType() } // GetOrderTypeByte gets this fulfillment's order's type byte. -func (f OrderFulfillment) GetOrderTypeByte() byte { +func (f orderFulfillment) GetOrderTypeByte() byte { return f.Order.GetOrderTypeByte() } // GetHoldAmount gets this fulfillment's order's hold amount. -func (f OrderFulfillment) GetHoldAmount() sdk.Coins { +func (f orderFulfillment) GetHoldAmount() sdk.Coins { return f.Order.GetHoldAmount() } // DistributeAssets records the distribution of assets in the provided amount to/from the given order. -func (f *OrderFulfillment) DistributeAssets(order OrderI, amount sdkmath.Int) error { +func (f *orderFulfillment) DistributeAssets(order OrderI, amount sdkmath.Int) error { if f.AssetsUnfilledAmt.LT(amount) { return fmt.Errorf("cannot fill %s order %d having assets left %q with %q from %s order %d: overfill", - f.GetOrderType(), f.GetOrderID(), f.GetAssetsUnfilled(), f.assetCoin(amount), order.GetOrderType(), order.GetOrderID()) + f.GetOrderType(), f.GetOrderID(), f.GetAssetsUnfilled(), f.AssetCoin(amount), order.GetOrderType(), order.GetOrderID()) } f.AssetsUnfilledAmt = f.AssetsUnfilledAmt.Sub(amount) f.AssetsFilledAmt = f.AssetsFilledAmt.Add(amount) - f.AssetDists = append(f.AssetDists, &Distribution{ + f.AssetDists = append(f.AssetDists, &distribution{ Address: order.GetOwner(), Amount: amount, }) return nil } -// DistributeAssets records the distribution of assets in the provided amount to/from the given fulfillments. -func DistributeAssets(of1, of2 *OrderFulfillment, amount sdkmath.Int) error { +// distributeAssets records the distribution of assets in the provided amount to/from the given fulfillments. +func distributeAssets(of1, of2 *orderFulfillment, amount sdkmath.Int) error { errs := []error{ of1.DistributeAssets(of2, amount), of2.DistributeAssets(of1, amount), @@ -230,23 +321,23 @@ func DistributeAssets(of1, of2 *OrderFulfillment, amount sdkmath.Int) error { } // DistributePrice records the distribution of price in the provided amount to/from the given order. -func (f *OrderFulfillment) DistributePrice(order OrderI, amount sdkmath.Int) error { +func (f *orderFulfillment) DistributePrice(order OrderI, amount sdkmath.Int) error { if f.PriceLeftAmt.LT(amount) && f.IsBidOrder() { return fmt.Errorf("cannot fill %s order %d having price left %q to %s order %d at a price of %q: overfill", - f.GetOrderType(), f.GetOrderID(), f.GetPriceLeft(), order.GetOrderType(), order.GetOrderID(), f.priceCoin(amount)) + f.GetOrderType(), f.GetOrderID(), f.GetPriceLeft(), order.GetOrderType(), order.GetOrderID(), f.PriceCoin(amount)) } f.PriceLeftAmt = f.PriceLeftAmt.Sub(amount) f.PriceAppliedAmt = f.PriceAppliedAmt.Add(amount) - f.PriceDists = append(f.PriceDists, &Distribution{ + f.PriceDists = append(f.PriceDists, &distribution{ Address: order.GetOwner(), Amount: amount, }) return nil } -// DistributePrice records the distribution of price in the provided amount to/from the given fulfillments. -func DistributePrice(of1, of2 *OrderFulfillment, amount sdkmath.Int) error { +// distributePrice records the distribution of price in the provided amount to/from the given fulfillments. +func distributePrice(of1, of2 *orderFulfillment, amount sdkmath.Int) error { errs := []error{ of1.DistributePrice(of2, amount), of2.DistributePrice(of1, amount), @@ -256,7 +347,7 @@ func DistributePrice(of1, of2 *OrderFulfillment, amount sdkmath.Int) error { // SplitOrder splits this order on the amount of assets filled. // This order fulfillment is updated to have the filled order, and the unfilled portion is returned. -func (f *OrderFulfillment) SplitOrder() (*Order, error) { +func (f *orderFulfillment) SplitOrder() (*Order, error) { filled, unfilled, err := f.Order.Split(f.AssetsFilledAmt) if err != nil { return nil, err @@ -269,7 +360,7 @@ func (f *OrderFulfillment) SplitOrder() (*Order, error) { } // AsFilledOrder creates a FilledOrder from this order fulfillment. -func (f OrderFulfillment) AsFilledOrder() *FilledOrder { +func (f orderFulfillment) AsFilledOrder() *FilledOrder { return NewFilledOrder(f.Order, f.GetPriceApplied(), f.FeesToPay) } @@ -283,7 +374,7 @@ func sumAssetsAndPrice(orders []*Order) (assets sdk.Coins, price sdk.Coins) { } // sumPriceLeft gets the sum of price left of the provided fulfillments. -func sumPriceLeft(fulfillments []*OrderFulfillment) sdkmath.Int { +func sumPriceLeft(fulfillments []*orderFulfillment) sdkmath.Int { rv := sdkmath.ZeroInt() for _, f := range fulfillments { rv = rv.Add(f.PriceLeftAmt) @@ -291,72 +382,6 @@ func sumPriceLeft(fulfillments []*OrderFulfillment) sdkmath.Int { return rv } -// Settlement contains information on how a set of orders is to be settled. -type Settlement struct { - // Transfers are all of the inputs and outputs needed to facilitate movement of assets and price. - Transfers []*Transfer - // FeeInputs are the inputs needed to facilitate payment of order fees. - FeeInputs []banktypes.Input - // FullyFilledOrders are all the orders that are fully filled in this settlement. - // If there is an order that's being partially filled, it will not be included in this list. - FullyFilledOrders []*FilledOrder - // PartialOrderFilled is a partially filled order with amounts indicating how much was filled. - // This is not included in FullyFilledOrders. - PartialOrderFilled *FilledOrder - // PartialOrderLeft is what's left of the partially filled order. - PartialOrderLeft *Order -} - -// BuildSettlement processes the provided orders, identifying how the provided orders can be settled. -func BuildSettlement(askOrders, bidOrders []*Order, sellerFeeRatioLookup func(denom string) (*FeeRatio, error)) (*Settlement, error) { - if err := validateCanSettle(askOrders, bidOrders); err != nil { - return nil, err - } - - askOFs := NewOrderFulfillments(askOrders) - bidOFs := NewOrderFulfillments(bidOrders) - - // Allocate the assets first. - if err := allocateAssets(askOFs, bidOFs); err != nil { - return nil, err - } - - settlement := &Settlement{} - // Identify any partial order and update its entry in the order fulfillments. - if err := splitPartial(askOFs, bidOFs, settlement); err != nil { - return nil, err - } - - // Allocate the prices. - if err := allocatePrice(askOFs, bidOFs); err != nil { - return nil, err - } - - // Set the fees in the fulfillments - sellerFeeRatio, err := sellerFeeRatioLookup(askOFs[0].GetPrice().Denom) - if err != nil { - return nil, err - } - if err = setFeesToPay(askOFs, bidOFs, sellerFeeRatio); err != nil { - return nil, err - } - - // Make sure everything adds up. - if err = validateFulfillments(askOFs, bidOFs); err != nil { - return nil, err - } - - // Create the transfers - if err = buildTransfers(askOFs, bidOFs, settlement); err != nil { - return nil, err - } - - // Indicate what's been filled in full and partially. - populateFilled(askOFs, bidOFs, settlement) - - return settlement, nil -} - // validateCanSettle does some superficial checking of the provided orders to make sure we can try to settle them. func validateCanSettle(askOrders, bidOrders []*Order) error { var errs []error @@ -421,14 +446,14 @@ func validateCanSettle(askOrders, bidOrders []*Order) error { } // allocateAssets distributes the assets among the fulfillments. -func allocateAssets(askOFs, bidOFs []*OrderFulfillment) error { +func allocateAssets(askOFs, bidOFs []*orderFulfillment) error { a, b := 0, 0 for a < len(askOFs) && b < len(bidOFs) { - assetsFilledAmt, err := GetFulfillmentAssetsAmt(askOFs[a], bidOFs[b]) + assetsFilledAmt, err := getFulfillmentAssetsAmt(askOFs[a], bidOFs[b]) if err != nil { return err } - if err = DistributeAssets(askOFs[a], bidOFs[b], assetsFilledAmt); err != nil { + if err = distributeAssets(askOFs[a], bidOFs[b], assetsFilledAmt); err != nil { return err } @@ -449,18 +474,30 @@ func allocateAssets(askOFs, bidOFs []*OrderFulfillment) error { return nil } +// getFulfillmentAssetsAmt figures out the assets that can be fulfilled with the two provided orders. +func getFulfillmentAssetsAmt(of1, of2 *orderFulfillment) (sdkmath.Int, error) { + if !of1.AssetsUnfilledAmt.IsPositive() || !of2.AssetsUnfilledAmt.IsPositive() { + return sdkmath.ZeroInt(), fmt.Errorf("cannot fill %s order %d having assets left %q "+ + "with %s order %d having assets left %q: zero or negative assets left", + of1.GetOrderType(), of1.GetOrderID(), of1.GetAssetsUnfilled(), + of2.GetOrderType(), of2.GetOrderID(), of2.GetAssetsUnfilled()) + } + + return MinSDKInt(of1.AssetsUnfilledAmt, of2.AssetsUnfilledAmt), nil +} + // splitPartial checks the provided fulfillments for a partial order and splits it out, updating the applicable fulfillment. // This will possibly populate the PartialOrderLeft in the provided Settlement. -func splitPartial(askOFs, bidOFs []*OrderFulfillment, settlement *Settlement) error { +func splitPartial(askOFs, bidOFs []*orderFulfillment, settlement *Settlement) error { if err := splitOrderFulfillments(askOFs, settlement); err != nil { return err } return splitOrderFulfillments(bidOFs, settlement) } -// splitOrderFulfillments checks each of the OrderFulfillment for partial (or incomplete) fills. -// If an appropriate partial fill is found, its OrderFulfillment is update and Settlement.PartialOrderLeft is set. -func splitOrderFulfillments(fulfillments []*OrderFulfillment, settlement *Settlement) error { +// splitOrderFulfillments checks each of the orderFulfillment for partial (or incomplete) fills. +// If an appropriate partial fill is found, its orderFulfillment is update and Settlement.PartialOrderLeft is set. +func splitOrderFulfillments(fulfillments []*orderFulfillment, settlement *Settlement) error { lastI := len(fulfillments) - 1 for i, f := range fulfillments { if f.AssetsFilledAmt.IsZero() { @@ -488,13 +525,13 @@ func splitOrderFulfillments(fulfillments []*OrderFulfillment, settlement *Settle } // allocatePrice distributes the prices among the fulfillments. -func allocatePrice(askOFs, bidOFs []*OrderFulfillment) error { +func allocatePrice(askOFs, bidOFs []*orderFulfillment) error { // Check that the total ask price is not more than the total bid price. totalAskPriceAmt := sumPriceLeft(askOFs) totalBidPriceAmt := sumPriceLeft(bidOFs) if totalAskPriceAmt.GT(totalBidPriceAmt) { return fmt.Errorf("total ask price %q is greater than total bid price %q", - askOFs[0].priceCoin(totalAskPriceAmt), bidOFs[0].priceCoin(totalBidPriceAmt)) + askOFs[0].PriceCoin(totalAskPriceAmt), bidOFs[0].PriceCoin(totalBidPriceAmt)) } // First pass at price distribution: Give all the asks their price. @@ -502,11 +539,11 @@ func allocatePrice(askOFs, bidOFs []*OrderFulfillment) error { totalFilledFirstPass := sdkmath.ZeroInt() for _, askOF := range askOFs { for askOF.PriceLeftAmt.IsPositive() && bidOFs[b].PriceLeftAmt.IsPositive() { - priceFilledAmt, err := GetFulfillmentPriceAmt(askOF, bidOFs[b]) + priceFilledAmt, err := getFulfillmentPriceAmt(askOF, bidOFs[b]) if err != nil { return err } - if err = DistributePrice(askOF, bidOFs[b], priceFilledAmt); err != nil { + if err = distributePrice(askOF, bidOFs[b], priceFilledAmt); err != nil { return err } totalFilledFirstPass = totalFilledFirstPass.Add(priceFilledAmt) @@ -548,8 +585,8 @@ func allocatePrice(askOFs, bidOFs []*OrderFulfillment) error { if b >= len(bidOFs) { panic(fmt.Errorf("total ask price %q, total bid price %q, difference %q, left to allocate %q: "+ "no bid orders left to allocate leftovers from", - askOFs[0].priceCoin(totalAskPriceAmt), bidOFs[0].priceCoin(totalBidPriceAmt), - askOFs[0].priceCoin(totalLeftoverPriceAmt), askOFs[0].priceCoin(leftoverPriceAmt))) + askOFs[0].PriceCoin(totalAskPriceAmt), bidOFs[0].PriceCoin(totalBidPriceAmt), + askOFs[0].PriceCoin(totalLeftoverPriceAmt), askOFs[0].PriceCoin(leftoverPriceAmt))) } // Figure out how much additional price this order should get. @@ -567,7 +604,7 @@ func allocatePrice(askOFs, bidOFs []*OrderFulfillment) error { // If it can't all come out of the current bid, use the rest of what this bid has and move to the next bid. for !addPriceAmt.IsZero() && b < len(bidOFs) && bidOFs[b].PriceLeftAmt.LTE(addPriceAmt) { bidPriceLeft := bidOFs[b].PriceLeftAmt - if err := DistributePrice(askOFs[a], bidOFs[b], bidPriceLeft); err != nil { + if err := distributePrice(askOFs[a], bidOFs[b], bidPriceLeft); err != nil { return err } addPriceAmt = addPriceAmt.Sub(bidPriceLeft) @@ -577,7 +614,7 @@ func allocatePrice(askOFs, bidOFs []*OrderFulfillment) error { // If there's still additional price left, it can all come out of the current bid. if !addPriceAmt.IsZero() && b < len(bidOFs) { - if err := DistributePrice(askOFs[a], bidOFs[b], addPriceAmt); err != nil { + if err := distributePrice(askOFs[a], bidOFs[b], addPriceAmt); err != nil { return err } leftoverPriceAmt = leftoverPriceAmt.Sub(addPriceAmt) @@ -591,8 +628,20 @@ func allocatePrice(askOFs, bidOFs []*OrderFulfillment) error { return nil } +// getFulfillmentPriceAmt figures out the price that can be fulfilled with the two provided orders. +func getFulfillmentPriceAmt(of1, of2 *orderFulfillment) (sdkmath.Int, error) { + if !of1.PriceLeftAmt.IsPositive() || !of2.PriceLeftAmt.IsPositive() { + return sdkmath.ZeroInt(), fmt.Errorf("cannot fill %s order %d having price left %q "+ + "with %s order %d having price left %q: zero or negative price left", + of1.GetOrderType(), of1.GetOrderID(), of1.GetPriceLeft(), + of2.GetOrderType(), of2.GetOrderID(), of2.GetPriceLeft()) + } + + return MinSDKInt(of1.PriceLeftAmt, of2.PriceLeftAmt), nil +} + // setFeesToPay sets the FeesToPay on each fulfillment. -func setFeesToPay(askOFs, bidOFs []*OrderFulfillment, sellerFeeRatio *FeeRatio) error { +func setFeesToPay(askOFs, bidOFs []*orderFulfillment, sellerFeeRatio *FeeRatio) error { var errs []error for _, askOF := range askOFs { feesToPay := askOF.GetSettlementFees() @@ -616,7 +665,7 @@ func setFeesToPay(askOFs, bidOFs []*OrderFulfillment, sellerFeeRatio *FeeRatio) } // validateFulfillments runs .Validate on each fulfillment, returning any problems. -func validateFulfillments(askOFs, bidOFs []*OrderFulfillment) error { +func validateFulfillments(askOFs, bidOFs []*orderFulfillment) error { var errs []error for _, askOF := range askOFs { if err := askOF.Validate(); err != nil { @@ -631,31 +680,69 @@ func validateFulfillments(askOFs, bidOFs []*OrderFulfillment) error { return errors.Join(errs...) } -// buildTransfers creates the transfers, inputs for fee payments, -// and fee total and sets those fields in the provided Settlement. -// This will populate the Transfers and FeeInputs fields in the provided Settlement. -func buildTransfers(askOFs, bidOFs []*OrderFulfillment, settlement *Settlement) error { - var errs []error - indexedFees := newIndexedAddrAmts() - transfers := make([]*Transfer, 0, len(askOFs)+len(bidOFs)) - - record := func(of *OrderFulfillment, getter func(fulfillment *OrderFulfillment) (*Transfer, error)) { - assetTrans, err := getter(of) - if err != nil { - errs = append(errs, err) - } else { - transfers = append(transfers, assetTrans) - } - - if !of.FeesToPay.IsZero() { - if of.FeesToPay.IsAnyNegative() { - errs = append(errs, fmt.Errorf("%s order %d cannot pay %q in fees: negative amount", - of.GetOrderType(), of.GetOrderID(), of.FeesToPay)) +// Validate makes sure the assets filled and price applied are acceptable for this fulfillment. +func (f orderFulfillment) Validate() (err error) { + defer func() { + if r := recover(); r != nil { + if e, ok := r.(error); ok { + err = e } else { - indexedFees.add(of.GetOwner(), of.FeesToPay...) + err = fmt.Errorf("%v", r) } } - } + }() + + orderPrice := f.GetPrice() + switch { + case f.IsAskOrder(): + if orderPrice.Amount.GT(f.PriceAppliedAmt) { + return fmt.Errorf("%s order %d price %q is more than price filled %q", + f.GetOrderType(), f.GetOrderID(), orderPrice, f.GetPriceApplied()) + } + case f.IsBidOrder(): + if !orderPrice.Amount.Equal(f.PriceAppliedAmt) { + return fmt.Errorf("%s order %d price %q is not equal to price filled %q", + f.GetOrderType(), f.GetOrderID(), orderPrice, f.GetPriceApplied()) + } + default: + // This is here in case something new implements SubOrderI but a case isn't added here. + panic(fmt.Errorf("%s order %d: unknown order type", f.GetOrderType(), f.GetOrderID())) + } + + orderAssets := f.GetAssets() + if !orderAssets.Amount.Equal(f.AssetsFilledAmt) { + return fmt.Errorf("%s order %d assets %q does not equal filled assets %q", + f.GetOrderType(), f.GetOrderID(), orderAssets, f.GetAssetsFilled()) + } + + return nil +} + +// buildTransfers creates the transfers, inputs for fee payments, +// and fee total and sets those fields in the provided Settlement. +// This will populate the Transfers and FeeInputs fields in the provided Settlement. +func buildTransfers(askOFs, bidOFs []*orderFulfillment, settlement *Settlement) error { + var errs []error + indexedFees := newIndexedAddrAmts() + transfers := make([]*Transfer, 0, len(askOFs)+len(bidOFs)) + + record := func(of *orderFulfillment, getter func(fulfillment *orderFulfillment) (*Transfer, error)) { + assetTrans, err := getter(of) + if err != nil { + errs = append(errs, err) + } else { + transfers = append(transfers, assetTrans) + } + + if !of.FeesToPay.IsZero() { + if of.FeesToPay.IsAnyNegative() { + errs = append(errs, fmt.Errorf("%s order %d cannot pay %q in fees: negative amount", + of.GetOrderType(), of.GetOrderID(), of.FeesToPay)) + } else { + indexedFees.add(of.GetOwner(), of.FeesToPay...) + } + } + } // If we got both the asset and price transfers from all OrderFulfillments, we'd be doubling // up on what's being traded. So we need to only get each from only one list. @@ -685,7 +772,7 @@ func buildTransfers(askOFs, bidOFs []*OrderFulfillment, settlement *Settlement) // populateFilled creates all the FilledOrder entries and stores them in the provided Settlement. // This will populate the FullyFilledOrders and PartialOrderFilled fields in the provided Settlement. -func populateFilled(askOFs, bidOFs []*OrderFulfillment, settlement *Settlement) { +func populateFilled(askOFs, bidOFs []*orderFulfillment, settlement *Settlement) { settlement.FullyFilledOrders = make([]*FilledOrder, 0, len(askOFs)+len(bidOFs)) for _, f := range askOFs { @@ -706,7 +793,7 @@ func populateFilled(askOFs, bidOFs []*OrderFulfillment, settlement *Settlement) } // getAssetTransfer gets the inputs and outputs to facilitate the transfers of assets for this order fulfillment. -func getAssetTransfer(f *OrderFulfillment) (*Transfer, error) { +func getAssetTransfer(f *orderFulfillment) (*Transfer, error) { assetsFilled := f.GetAssetsFilled() if !assetsFilled.Amount.IsPositive() { return nil, fmt.Errorf("%s order %d cannot be filled with %q assets: amount not positive", @@ -718,15 +805,15 @@ func getAssetTransfer(f *OrderFulfillment) (*Transfer, error) { for _, dist := range f.AssetDists { if !dist.Amount.IsPositive() { return nil, fmt.Errorf("%s order %d cannot have %q assets in a transfer: amount not positive", - f.GetOrderType(), f.GetOrderID(), f.assetCoin(dist.Amount)) + f.GetOrderType(), f.GetOrderID(), f.AssetCoin(dist.Amount)) } - indexedDists.add(dist.Address, f.assetCoin(dist.Amount)) + indexedDists.add(dist.Address, f.AssetCoin(dist.Amount)) sumDists = sumDists.Add(dist.Amount) } if !sumDists.Equal(assetsFilled.Amount) { return nil, fmt.Errorf("%s order %d assets filled %q does not equal assets distributed %q", - f.GetOrderType(), f.GetOrderID(), assetsFilled, f.assetCoin(sumDists)) + f.GetOrderType(), f.GetOrderID(), assetsFilled, f.AssetCoin(sumDists)) } if f.IsAskOrder() { @@ -747,7 +834,7 @@ func getAssetTransfer(f *OrderFulfillment) (*Transfer, error) { } // getPriceTransfer gets the inputs and outputs to facilitate the transfers for the price of this order fulfillment. -func getPriceTransfer(f *OrderFulfillment) (*Transfer, error) { +func getPriceTransfer(f *orderFulfillment) (*Transfer, error) { priceApplied := f.GetPriceApplied() if !priceApplied.Amount.IsPositive() { return nil, fmt.Errorf("%s order %d cannot be filled at price %q: amount not positive", @@ -759,15 +846,15 @@ func getPriceTransfer(f *OrderFulfillment) (*Transfer, error) { for _, dist := range f.PriceDists { if !dist.Amount.IsPositive() { return nil, fmt.Errorf("%s order %d cannot have price %q in a transfer: amount not positive", - f.GetOrderType(), f.GetOrderID(), f.priceCoin(dist.Amount)) + f.GetOrderType(), f.GetOrderID(), f.PriceCoin(dist.Amount)) } - indexedDists.add(dist.Address, f.priceCoin(dist.Amount)) + indexedDists.add(dist.Address, f.PriceCoin(dist.Amount)) sumDists = sumDists.Add(dist.Amount) } if !sumDists.Equal(priceApplied.Amount) { return nil, fmt.Errorf("%s order %d price filled %q does not equal price distributed %q", - f.GetOrderType(), f.GetOrderID(), priceApplied, f.priceCoin(sumDists)) + f.GetOrderType(), f.GetOrderID(), priceApplied, f.PriceCoin(sumDists)) } if f.IsAskOrder() { @@ -786,792 +873,3 @@ func getPriceTransfer(f *OrderFulfillment) (*Transfer, error) { // This is here in case a new SubTypeI is made that isn't accounted for in here. panic(fmt.Errorf("%s order %d: unknown order type", f.GetOrderType(), f.GetOrderID())) } - -// Apply adjusts this order fulfillment using the provided info. -func (f *OrderFulfillment) Apply(order *OrderFulfillment, assetsAmt, priceAmt sdkmath.Int) error { - assets := sdk.NewCoin(order.GetAssets().Denom, assetsAmt) - price := sdk.NewCoin(order.GetPrice().Denom, priceAmt) - - newAssetsUnfilledAmt := f.AssetsUnfilledAmt.Sub(assetsAmt) - if newAssetsUnfilledAmt.IsNegative() { - return fmt.Errorf("cannot fill %s order %d having assets left %q with %q from %s order %d: overfill", - f.GetOrderType(), f.GetOrderID(), f.GetAssetsUnfilled(), assets, order.GetOrderType(), order.GetOrderID()) - } - - newPriceLeftAmt := f.PriceLeftAmt.Sub(priceAmt) - // ask orders are allow to go negative on price left, but bid orders are not. - if newPriceLeftAmt.IsNegative() && f.IsBidOrder() { - return fmt.Errorf("cannot fill %s order %d having price left %q to %s order %d at a price of %q: overfill", - f.GetOrderType(), f.GetOrderID(), f.GetPriceLeft(), order.GetOrderType(), order.GetOrderID(), price) - } - - f.AssetsUnfilledAmt = newAssetsUnfilledAmt - f.AssetsFilledAmt = f.AssetsFilledAmt.Add(assetsAmt) - f.PriceLeftAmt = newPriceLeftAmt - f.PriceAppliedAmt = f.PriceAppliedAmt.Add(priceAmt) - f.Splits = append(f.Splits, &OrderSplit{ - Order: order, - Assets: assets, - Price: price, - }) - return nil -} - -// ApplyLeftoverPrice increases this fulfillment and the provided split -// using info in the split and the provided amount. -func (f *OrderFulfillment) ApplyLeftoverPrice(askSplit *OrderSplit, amt sdkmath.Int) { - // Update this fulfillment to indicate that the amount has been applied. - f.PriceLeftAmt = f.PriceLeftAmt.Sub(amt) - f.PriceAppliedAmt = f.PriceAppliedAmt.Add(amt) - - // Update the ask split to include the extra amount. - askSplit.Price.Amount = askSplit.Price.Amount.Add(amt) - // And update the ask split's fulfillment similarly. - askSplit.Order.PriceLeftAmt = askSplit.Order.PriceLeftAmt.Sub(amt) - askSplit.Order.PriceAppliedAmt = askSplit.Order.PriceAppliedAmt.Add(amt) - - // Update the bid split entry for this order in the splits that the ask split has - // to indicate the extra amount from this bid. - orderID := f.GetOrderID() - for _, bidSplit := range askSplit.Order.Splits { - if bidSplit.Order.GetOrderID() == orderID { - bidSplit.Price.Amount = bidSplit.Price.Amount.Add(amt) - return - } - } - - // If we didn't find a bid split to update, something is horribly wrong. - panic(fmt.Errorf("could not apply leftover amount %s from %s order %d to %s order %d: bid split not found", - amt, f.GetOrderType(), orderID, askSplit.Order.GetOrderType(), askSplit.Order.GetOrderID())) -} - -// Finalize does some final calculations and validation for this order fulfillment. -// This order fulfillment and the ones in it maybe updated during this. -func (f *OrderFulfillment) Finalize(sellerFeeRatio *FeeRatio) (err error) { - // If this is returning an error, unset all the fields that get set in here. - defer func() { - if err != nil { - f.IsFinalized = false - f.PriceFilledAmt = sdkmath.ZeroInt() - f.PriceUnfilledAmt = sdkmath.ZeroInt() - f.FeesToPay = nil - f.OrderFeesLeft = nil - } - }() - - // AssetsFilledAmt cannot be zero here because we'll be dividing by it. - // AssetsFilledAmt cannot be negative here because we can't have negative values from the calcs. - // Checking for assets filled > zero here (instead of in Validate2) because we need to divide by it in here. - if !f.AssetsFilledAmt.IsPositive() { - return fmt.Errorf("no assets filled in %s order %d", f.GetOrderType(), f.GetOrderID()) - } - - isAskOrder, isBidOrder := f.IsAskOrder(), f.IsBidOrder() - isFullyFilled := f.IsFullyFilled() - orderFees := f.GetSettlementFees() - orderAssets := f.GetAssets() - orderPrice := f.GetPrice() - - f.PriceFilledAmt = orderPrice.Amount - f.PriceUnfilledAmt = sdkmath.ZeroInt() - f.FeesToPay = orderFees - f.OrderFeesLeft = nil - - if !isFullyFilled { - // Make sure the price can be split on a whole number, and figure out the price being filled. - priceAssets := orderPrice.Amount.Mul(f.AssetsFilledAmt) - priceRem := priceAssets.Mod(orderAssets.Amount) - if !priceRem.IsZero() { - return fmt.Errorf("%s order %d having assets %q cannot be partially filled by %q: "+ - "price %q is not evenly divisible", - f.GetOrderType(), f.GetOrderID(), orderAssets, f.GetAssetsFilled(), orderPrice) - } - f.PriceFilledAmt = priceAssets.Quo(orderAssets.Amount) - f.PriceUnfilledAmt = orderPrice.Amount.Sub(f.PriceFilledAmt) - - // Make sure the fees can be split on a whole number, and figure out how much is actually being paid of them. - f.FeesToPay = nil - for _, orderFee := range orderFees { - feeAssets := orderFee.Amount.Mul(f.AssetsFilledAmt) - feeRem := feeAssets.Mod(orderAssets.Amount) - if !feeRem.IsZero() { - return fmt.Errorf("%s order %d having assets %q cannot be partially filled by %q: "+ - "fee %q is not evenly divisible", - f.GetOrderType(), f.GetOrderID(), orderAssets, f.GetAssetsFilled(), orderFee) - } - feeAmtToPay := feeAssets.Quo(orderAssets.Amount) - f.FeesToPay = f.FeesToPay.Add(sdk.NewCoin(orderFee.Denom, feeAmtToPay)) - } - f.OrderFeesLeft = orderFees.Sub(f.FeesToPay...) - } - - switch { - case isAskOrder: - // For ask orders, we need to calculate and add the ratio fee to the fees to pay. - // This should NOT affect the order fees left. - if sellerFeeRatio != nil { - ratioFeeToPay, ferr := sellerFeeRatio.ApplyToLoosely(f.GetPriceApplied()) - if ferr != nil { - return fmt.Errorf("could not calculate %s order %d ratio fee: %w", - f.GetOrderType(), f.GetOrderID(), ferr) - } - f.FeesToPay = f.FeesToPay.Add(ratioFeeToPay) - } - case isBidOrder: - // When adding things to PriceAppliedAmt (and Splits .Price), we used truncation on the divisions. - // When calculated PriceFilledAmt, we made sure it was a whole number based on total assets being distributed. - // So, at this point, PriceAppliedAmt might be a little less than the PriceFilledAmt. - // If that's the case, we'll distribute the difference among the splits. - toDistribute := f.PriceFilledAmt.Sub(f.PriceAppliedAmt) - if toDistribute.IsPositive() { - distLeft := toDistribute - // First, go through each split, and apply the leftovers to any asks that still have price left. - for _, askSplit := range f.Splits { - if askSplit.Order.PriceLeftAmt.IsPositive() { - toDist := MinSDKInt(askSplit.Order.PriceLeftAmt, distLeft) - f.ApplyLeftoverPrice(askSplit, toDist) - distLeft = distLeft.Sub(toDist) - } - } - - // Now try to distribute the leftovers evenly weighted by assets. - // First pass, we won't default to 1 (if the calc comes up zero). - // This helps weigh larger orders that are at the end of the list. - // Once they've all had a chance, use a minimum of 1. - minOne := false - for distLeft.IsPositive() { - for _, askSplit := range f.Splits { - distAmt := toDistribute.Mul(askSplit.Assets.Amount).Quo(f.AssetsFilledAmt) - if distAmt.IsZero() { - if !minOne { - continue - } - distAmt = sdkmath.OneInt() - } - distAmt = MinSDKInt(distAmt, distLeft) - - f.ApplyLeftoverPrice(askSplit, distAmt) - - distLeft = distLeft.Sub(distAmt) - if !distLeft.IsPositive() { - break - } - } - minOne = true - } - } - } - - f.IsFinalized = true - return nil -} - -// Validate makes sure the assets filled and price applied are acceptable for this fulfillment. -func (f OrderFulfillment) Validate() (err error) { - defer func() { - if r := recover(); r != nil { - if e, ok := r.(error); ok { - err = e - } else { - err = fmt.Errorf("%v", r) - } - } - }() - - orderPrice := f.GetPrice() - switch { - case f.IsAskOrder(): - if orderPrice.Amount.GT(f.PriceAppliedAmt) { - return fmt.Errorf("%s order %d price %q is more than price filled %q", - f.GetOrderType(), f.GetOrderID(), orderPrice, f.GetPriceApplied()) - } - case f.IsBidOrder(): - if !orderPrice.Amount.Equal(f.PriceAppliedAmt) { - return fmt.Errorf("%s order %d price %q is not equal to price filled %q", - f.GetOrderType(), f.GetOrderID(), orderPrice, f.GetPriceApplied()) - } - default: - // This is here in case something new implements SubOrderI but a case isn't added here. - panic(fmt.Errorf("%s order %d: unknown order type", f.GetOrderType(), f.GetOrderID())) - } - - orderAssets := f.GetAssets() - if !orderAssets.Amount.Equal(f.AssetsFilledAmt) { - return fmt.Errorf("%s order %d assets %q does not equal filled assets %q", - f.GetOrderType(), f.GetOrderID(), orderAssets, f.GetAssetsFilled()) - } - - return nil -} - -// Validate2 does some final validation and sanity checking on this order fulfillment. -// It's assumed that Finalize has been called before calling this. -func (f OrderFulfillment) Validate2() error { - if _, err := f.Order.GetSubOrder(); err != nil { - return err - } - if !f.IsFinalized { - return fmt.Errorf("fulfillment for %s order %d has not been finalized", f.GetOrderType(), f.GetOrderID()) - } - - orderAssets := f.GetAssets() - if f.AssetsUnfilledAmt.IsNegative() { - return fmt.Errorf("%s order %d having assets %q has negative assets left %q after filling %q", - f.GetOrderType(), f.GetOrderID(), orderAssets, f.GetAssetsUnfilled(), f.GetAssetsFilled()) - } - if !f.AssetsFilledAmt.IsPositive() { - return fmt.Errorf("cannot fill non-positive assets %q on %s order %d having assets %q", - f.GetAssetsFilled(), f.GetOrderType(), f.GetOrderID(), orderAssets) - } - trackedAssetsAmt := f.AssetsFilledAmt.Add(f.AssetsUnfilledAmt) - if !orderAssets.Amount.Equal(trackedAssetsAmt) { - return fmt.Errorf("tracked assets %q does not equal %s order %d assets %q", - sdk.Coin{Denom: orderAssets.Denom, Amount: trackedAssetsAmt}, f.GetOrderType(), f.GetOrderID(), orderAssets) - } - - orderPrice := f.GetPrice() - if f.PriceLeftAmt.GTE(orderPrice.Amount) { - return fmt.Errorf("price left %q is not less than %s order %d price %q", - f.GetPriceLeft(), f.GetOrderType(), f.GetOrderID(), orderPrice) - } - if !f.PriceAppliedAmt.IsPositive() { - return fmt.Errorf("cannot apply non-positive price %q to %s order %d having price %q", - f.GetPriceApplied(), f.GetOrderType(), f.GetOrderID(), orderPrice) - } - trackedPriceAmt := f.PriceAppliedAmt.Add(f.PriceLeftAmt) - if !orderPrice.Amount.Equal(trackedPriceAmt) { - return fmt.Errorf("tracked price %q does not equal %s order %d price %q", - sdk.Coin{Denom: orderPrice.Denom, Amount: trackedPriceAmt}, f.GetOrderType(), f.GetOrderID(), orderPrice) - } - if !f.PriceFilledAmt.IsPositive() { - return fmt.Errorf("cannot fill %s order %d having price %q with non-positive price %q", - f.GetOrderType(), f.GetOrderID(), orderPrice, f.GetPriceFilled()) - } - totalPriceAmt := f.PriceFilledAmt.Add(f.PriceUnfilledAmt) - if !orderPrice.Amount.Equal(totalPriceAmt) { - return fmt.Errorf("filled price %q plus unfilled price %q does not equal order price %q for %s order %d", - f.GetPriceFilled(), f.GetPriceUnfilled(), orderPrice, f.GetOrderType(), f.GetOrderID()) - } - if f.PriceUnfilledAmt.IsNegative() { - return fmt.Errorf("%s order %d having price %q has negative price %q after filling %q", - f.GetOrderType(), f.GetOrderID(), orderPrice, f.GetPriceUnfilled(), f.GetPriceFilled()) - } - - if len(f.Splits) == 0 { - return fmt.Errorf("no splits applied to %s order %d", f.GetOrderType(), f.GetOrderID()) - } - - var splitsAssets, splitsPrice sdk.Coins - for _, split := range f.Splits { - splitsAssets = splitsAssets.Add(split.Assets) - splitsPrice = splitsPrice.Add(split.Price) - } - - if len(splitsAssets) != 1 { - return fmt.Errorf("multiple asset denoms %q in splits applied to %s order %d having assets %q", - splitsAssets, f.GetOrderType(), f.GetOrderID(), orderAssets) - } - if splitsAssets[0].Denom != orderAssets.Denom { - return fmt.Errorf("splits asset denom %q does not equal order assets denom %q on %s order %d", - splitsAssets, orderAssets, f.GetOrderType(), f.GetOrderID()) - } - if !splitsAssets[0].Amount.Equal(f.AssetsFilledAmt) { - return fmt.Errorf("splits asset total %q does not equal filled assets %q on %s order %d", - splitsAssets, f.GetAssetsFilled(), f.GetOrderType(), f.GetOrderID()) - } - - if len(splitsPrice) != 1 { - return fmt.Errorf("multiple price denoms %q in splits applied to %s order %d having price %q", - splitsPrice, f.GetOrderType(), f.GetOrderID(), orderPrice) - } - if splitsPrice[0].Denom != orderPrice.Denom { - return fmt.Errorf("splits price denom %q does not equal order price denom %q on %s order %d", - splitsPrice, orderPrice, f.GetOrderType(), f.GetOrderID()) - } - if !splitsPrice[0].Amount.Equal(f.PriceAppliedAmt) { - return fmt.Errorf("splits price total %q does not equal filled price %q on %s order %d", - splitsPrice, f.GetPriceApplied(), f.GetOrderType(), f.GetOrderID()) - } - - orderFees := f.GetSettlementFees() - if f.OrderFeesLeft.IsAnyNegative() { - return fmt.Errorf("settlement fees left %q is negative for %s order %d having fees %q", - f.OrderFeesLeft, f.GetOrderType(), f.GetOrderID(), orderFees) - } - if _, hasNeg := orderFees.SafeSub(f.OrderFeesLeft...); hasNeg { - return fmt.Errorf("settlement fees left %q is greater than %s order %d settlement fees %q", - f.OrderFeesLeft, f.GetOrderType(), f.GetOrderID(), orderFees) - } - - isFullyFilled := f.IsFullyFilled() - if isFullyFilled { - // IsFullyFilled returns true if unfilled assets is zero or negative. - // We know from a previous check that unfilled is not negative. - // So here, we know it's zero, and don't need to check that again. - - if !f.PriceUnfilledAmt.IsZero() { - return fmt.Errorf("fully filled %s order %d has non-zero unfilled price %q", - f.GetOrderType(), f.GetOrderID(), f.GetPriceUnfilled()) - } - if !f.OrderFeesLeft.IsZero() { - return fmt.Errorf("fully filled %s order %d has non-zero settlement fees left %q", - f.GetOrderType(), f.GetOrderID(), f.OrderFeesLeft) - } - } - - switch { - case f.IsAskOrder(): - // For ask orders, the applied amount needs to be at least the filled amount. - if f.PriceAppliedAmt.LT(f.PriceFilledAmt) { - return fmt.Errorf("%s order %d having assets %q and price %q cannot be filled by %q at price %q: insufficient price", - f.GetOrderType(), f.GetOrderID(), orderAssets, orderPrice, f.GetAssetsFilled(), f.GetPriceApplied()) - } - - // If not being fully filled on an order that has some fees, make sure that there's at most 1 denom in the fees left. - if !isFullyFilled && len(orderFees) > 0 && len(f.OrderFeesLeft) > 1 { - return fmt.Errorf("partial fulfillment for %s order %d having seller settlement fees %q has multiple denoms in fees left %q", - f.GetOrderType(), f.GetOrderID(), orderFees, f.OrderFeesLeft) - } - - // For ask orders, the tracked fees must be at least the order fees. - trackedFees := f.FeesToPay.Add(f.OrderFeesLeft...) - if _, hasNeg := trackedFees.SafeSub(orderFees...); hasNeg { - return fmt.Errorf("tracked settlement fees %q is less than %s order %d settlement fees %q", - trackedFees, f.GetOrderType(), f.GetOrderID(), orderFees) - } - case f.IsBidOrder(): - if !f.PriceAppliedAmt.Equal(f.PriceFilledAmt) { - return fmt.Errorf("price applied %q does not equal price filled %q for %s order %d having price %q", - f.GetPriceApplied(), f.GetPriceFilled(), f.GetOrderType(), f.GetOrderID(), orderPrice) - } - - // We now know that price applied = filled, and applied + left = order = filled + unfilled, so left = unfilled too. - // We also know that applied > 0 and left < order. So 0 < applied < order. - // If fully filled, we know that unfilled = 0, so applied = order = filled, so we don't need to check that again here. - // If partially filled, we know that applied < order, so we don't need to check that either. - - // For bid orders, fees to pay + fees left should equal the order fees. - trackedFees := f.FeesToPay.Add(f.OrderFeesLeft...) - if !CoinsEquals(trackedFees, orderFees) { - return fmt.Errorf("tracked settlement fees %q does not equal %s order %d settlement fees %q", - trackedFees, f.GetOrderType(), f.GetOrderID(), orderFees) - } - default: - // The only way to trigger this would be to add a new order type but not add a case for it in this switch. - panic(fmt.Errorf("case missing for %T in Validate2", f.GetOrderType())) - } - - // Saving this simple check for last in the hopes that a previous error exposes why this - // order might accidentally be only partially filled. - if !isFullyFilled && !f.PartialFillAllowed() { - return fmt.Errorf("cannot fill %s order %d having assets %q with %q: order does not allow partial fill", - f.GetOrderType(), f.GetOrderID(), orderAssets, f.GetAssetsFilled()) - } - - return nil -} - -// Fulfill attempts to use the two provided order fulfillments to fulfill each other. -// The provided order fulfillments will be updated if everything goes okay. -func Fulfill(of1, of2 *OrderFulfillment) error { - order1Type := of1.Order.GetOrderType() - order2Type := of2.Order.GetOrderType() - if order1Type == order2Type { - return fmt.Errorf("cannot fulfill %s order %d with %s order %d: order type mismatch", - order1Type, of1.Order.OrderId, order2Type, of2.Order.OrderId) - } - - var askOF, bidOF *OrderFulfillment - if order1Type == OrderTypeAsk { - askOF = of1 - bidOF = of2 - } else { - askOF = of2 - bidOF = of1 - } - - askOrder, bidOrder := askOF.Order.GetAskOrder(), bidOF.Order.GetBidOrder() - if askOrder.Assets.Denom != bidOrder.Assets.Denom { - return fmt.Errorf("cannot fill bid order %d having assets %q with ask order %d having assets %q: denom mismatch", - bidOF.GetOrderID(), bidOrder.Assets, askOF.GetOrderID(), askOrder.Assets) - } - if askOrder.Price.Denom != bidOrder.Price.Denom { - return fmt.Errorf("cannot fill ask order %d having price %q with bid order %d having price %q: denom mismatch", - askOF.GetOrderID(), askOrder.Price, bidOF.GetOrderID(), bidOrder.Price) - } - - assetsAmt, err := GetFulfillmentAssetsAmt(askOF, bidOF) - if err != nil { - return err - } - - // We calculate the price amount based off the original bid order assets (as opposed to assets left) - // for consistent truncation and remainders. Once we've identified all the fulfillment relationships, - // we'll enumerate and redistribute those remainders. - priceAmt := bidOrder.Price.Amount - if !assetsAmt.Equal(bidOrder.Assets.Amount) { - priceAmt = bidOrder.Price.Amount.Mul(assetsAmt).Quo(bidOrder.Assets.Amount) - } - - askErr := askOF.Apply(bidOF, assetsAmt, priceAmt) - bidErr := bidOF.Apply(askOF, assetsAmt, priceAmt) - - return errors.Join(askErr, bidErr) -} - -// GetFulfillmentAssetsAmt figures out the assets that can be fulfilled with the two provided orders. -func GetFulfillmentAssetsAmt(of1, of2 *OrderFulfillment) (sdkmath.Int, error) { - if !of1.AssetsUnfilledAmt.IsPositive() || !of2.AssetsUnfilledAmt.IsPositive() { - return sdkmath.ZeroInt(), fmt.Errorf("cannot fill %s order %d having assets left %q "+ - "with %s order %d having assets left %q: zero or negative assets left", - of1.GetOrderType(), of1.GetOrderID(), of1.GetAssetsUnfilled(), - of2.GetOrderType(), of2.GetOrderID(), of2.GetAssetsUnfilled()) - } - - return MinSDKInt(of1.AssetsUnfilledAmt, of2.AssetsUnfilledAmt), nil -} - -// GetFulfillmentPriceAmt figures out the price that can be fulfilled with the two provided orders. -func GetFulfillmentPriceAmt(of1, of2 *OrderFulfillment) (sdkmath.Int, error) { - if !of1.PriceLeftAmt.IsPositive() || !of2.PriceLeftAmt.IsPositive() { - return sdkmath.ZeroInt(), fmt.Errorf("cannot fill %s order %d having price left %q "+ - "with %s order %d having price left %q: zero or negative price left", - of1.GetOrderType(), of1.GetOrderID(), of1.GetPriceLeft(), - of2.GetOrderType(), of2.GetOrderID(), of2.GetPriceLeft()) - } - - return MinSDKInt(of1.PriceLeftAmt, of2.PriceLeftAmt), nil -} - -// Fulfillments contains information on how orders are to be fulfilled. -type Fulfillments struct { - // AskOFs are all the ask orders and how they are to be filled. - AskOFs []*OrderFulfillment - // BidOFs are all the bid orders and how they are to be filled. - BidOFs []*OrderFulfillment - // PartialOrder contains info on an order that is only being partially filled. - // The transfers For part of its funds are included in the order fulfillments. - PartialOrder *PartialFulfillment -} - -// PartialFulfillment contains the remains of a partially filled order, and info on what was filled. -type PartialFulfillment struct { - // NewOrder is an updated version of the partially filled order with reduced amounts. - NewOrder *Order - // AssetsFilled is the amount of order assets that were filled. - AssetsFilled sdk.Coin - // PriceFilled is the amount of the order price that was filled. - PriceFilled sdk.Coin -} - -// NewPartialFulfillment creates a new PartialFulfillment using the provided OrderFulfillment information. -func NewPartialFulfillment(f *OrderFulfillment) *PartialFulfillment { - rv := &PartialFulfillment{ - NewOrder: NewOrder(f.GetOrderID()), - AssetsFilled: f.GetAssetsFilled(), - PriceFilled: f.GetPriceFilled(), - } - - if f.IsAskOrder() { - askOrder := &AskOrder{ - MarketId: f.GetMarketID(), - Seller: f.GetOwner(), - Assets: f.GetAssetsUnfilled(), - Price: f.GetPriceUnfilled(), - AllowPartial: f.PartialFillAllowed(), - } - if !f.OrderFeesLeft.IsZero() { - if len(f.OrderFeesLeft) > 1 { - panic(fmt.Errorf("partially filled ask order %d somehow has multiple denoms in fees left %q", - f.GetOrderID(), f.OrderFeesLeft)) - } - askOrder.SellerSettlementFlatFee = &f.OrderFeesLeft[0] - } - rv.NewOrder.WithAsk(askOrder) - return rv - } - - if f.IsBidOrder() { - bidOrder := &BidOrder{ - MarketId: f.GetMarketID(), - Buyer: f.GetOwner(), - Assets: f.GetAssetsUnfilled(), - Price: f.GetPriceUnfilled(), - BuyerSettlementFees: f.OrderFeesLeft, - AllowPartial: f.PartialFillAllowed(), - } - rv.NewOrder.WithBid(bidOrder) - return rv - } - - // This is here in case another order type is created, but a case for it isn't added to this func. - panic(fmt.Errorf("order %d has unknown type %q", f.GetOrderID(), f.GetOrderType())) -} - -// BuildFulfillments creates all of the ask and bid order fulfillments. -func BuildFulfillments(askOrders, bidOrders []*Order, sellerFeeRatio *FeeRatio) (*Fulfillments, error) { - askOFs := make([]*OrderFulfillment, len(askOrders)) - for i, askOrder := range askOrders { - if !askOrder.IsAskOrder() { - return nil, fmt.Errorf("%s order %d is not an ask order but is in the askOrders list", - askOrder.GetOrderType(), askOrder.GetOrderID()) - } - askOFs[i] = NewOrderFulfillment(askOrder) - } - bidOFs := make([]*OrderFulfillment, len(bidOrders)) - for i, bidOrder := range bidOrders { - if !bidOrder.IsBidOrder() { - return nil, fmt.Errorf("%s order %d is not a bid order but is in the bidOrders list", - bidOrder.GetOrderType(), bidOrder.GetOrderID()) - } - bidOFs[i] = NewOrderFulfillment(bidOrder) - } - - var a, b int - for a < len(askOFs) && b < len(bidOFs) { - err := Fulfill(askOFs[a], bidOFs[b]) - if err != nil { - return nil, err - } - askFilled := askOFs[a].IsFullyFilled() - bidFilled := bidOFs[b].IsFullyFilled() - if !askFilled && !bidFilled { - return nil, fmt.Errorf("neither %s order %d nor %s order %d could be filled in full", - askOFs[a].GetOrderType(), askOFs[a].GetOrderID(), bidOFs[b].GetOrderType(), bidOFs[b].GetOrderID()) - } - if askFilled { - a++ - } - if bidFilled { - b++ - } - } - - // Finalize all the fulfillments. - // Need to finalize bid orders first due to possible extra price distribution. - for _, bidOF := range bidOFs { - if err := bidOF.Finalize(sellerFeeRatio); err != nil { - return nil, err - } - } - for _, askOF := range askOFs { - if err := askOF.Finalize(sellerFeeRatio); err != nil { - return nil, err - } - } - - // And make sure they're all valid. - for _, askOF := range askOFs { - if err := askOF.Validate2(); err != nil { - return nil, err - } - } - for _, bidOF := range bidOFs { - if err := bidOF.Validate2(); err != nil { - return nil, err - } - } - - // Make sure none of them are partially filled except possibly the last in each list. - var partialFulfillments []*OrderFulfillment - lastAskI, lastBidI := len(askOFs)-1, len(bidOFs)-1 - for i, askOF := range askOFs { - if !askOF.IsFullyFilled() { - if i != lastAskI { - return nil, fmt.Errorf("%s order %d (at index %d) is not filled in full and is not the last ask order provided", - askOF.GetOrderType(), askOF.GetOrderID(), i) - } - partialFulfillments = append(partialFulfillments, askOF) - } - } - for i, bidOF := range bidOFs { - if !bidOF.IsFullyFilled() { - if i != lastBidI { - return nil, fmt.Errorf("%s order %d (at index %d) is not filled in full and is not the last bid order provided", - bidOF.GetOrderType(), bidOF.GetOrderID(), i) - } - partialFulfillments = append(partialFulfillments, bidOF) - } - } - - // And make sure that only one order is being partially filled. - if len(partialFulfillments) > 1 { - return nil, fmt.Errorf("%s order %d and %s order %d cannot both be partially filled", - partialFulfillments[0].GetOrderType(), partialFulfillments[0].GetOrderID(), - partialFulfillments[1].GetOrderType(), partialFulfillments[1].GetOrderID()) - } - - rv := &Fulfillments{ - AskOFs: askOFs, - BidOFs: bidOFs, - } - - if len(partialFulfillments) > 0 { - rv.PartialOrder = NewPartialFulfillment(partialFulfillments[0]) - } - - return rv, nil -} - -// indexedAddrAmts is a set of addresses and amounts. -type indexedAddrAmts struct { - // addrs are a list of all addresses that have amounts. - addrs []string - // amts are a list of the coin amounts for each address (by slice index). - amts []sdk.Coins - // indexes are the index value for each address. - indexes map[string]int -} - -func newIndexedAddrAmts() *indexedAddrAmts { - return &indexedAddrAmts{ - indexes: make(map[string]int), - } -} - -// add adds the coins to the given address. -// Panics if a provided coin is invalid. -func (i *indexedAddrAmts) add(addr string, coins ...sdk.Coin) { - for _, coin := range coins { - if err := coin.Validate(); err != nil { - panic(fmt.Errorf("cannot index and add invalid coin amount %q", coin)) - } - } - n, known := i.indexes[addr] - if !known { - n = len(i.addrs) - i.indexes[addr] = n - i.addrs = append(i.addrs, addr) - i.amts = append(i.amts, sdk.NewCoins()) - } - i.amts[n] = i.amts[n].Add(coins...) -} - -// getAsInputs returns all the entries as bank Inputs. -// Panics if this is nil, has no addrs, or has a negative coin amount. -func (i *indexedAddrAmts) getAsInputs() []banktypes.Input { - if i == nil || len(i.addrs) == 0 { - return nil - } - rv := make([]banktypes.Input, len(i.addrs)) - for n, addr := range i.addrs { - if !i.amts[n].IsAllPositive() { - panic(fmt.Errorf("invalid indexed amount %q for address %q: cannot be zero or negative", addr, i.amts[n])) - } - rv[n] = banktypes.Input{Address: addr, Coins: i.amts[n]} - } - return rv -} - -// getAsOutputs returns all the entries as bank Outputs. -// Panics if this is nil, has no addrs, or has a negative coin amount. -func (i *indexedAddrAmts) getAsOutputs() []banktypes.Output { - if i == nil || len(i.addrs) == 0 { - return nil - } - rv := make([]banktypes.Output, len(i.addrs)) - for n, addr := range i.addrs { - if !i.amts[n].IsAllPositive() { - panic(fmt.Errorf("invalid indexed amount %q for address %q: cannot be zero or negative", addr, i.amts[n])) - } - rv[n] = banktypes.Output{Address: addr, Coins: i.amts[n]} - } - return rv -} - -// Transfer contains bank inputs and outputs indicating a transfer that needs to be made. -type Transfer struct { - // Inputs are the inputs that make up this transfer. - Inputs []banktypes.Input - // Outputs are the outputs that make up this transfer. - Outputs []banktypes.Output -} - -// SettlementTransfers has everything needed to do all the transfers for a settlement. -type SettlementTransfers struct { - // OrderTransfers are all of the asset and price transfers needed to facilitate a settlement. - OrderTransfers []*Transfer - // FeeInputs are all of the inputs needed to facilitate payment of fees to a market. - FeeInputs []banktypes.Input -} - -// BuildSettlementTransfers creates all the order transfers needed for the provided fulfillments. -// Assumes that all fulfillments have passed Validate2. -// Panics if any amounts are negative. -func BuildSettlementTransfers(f *Fulfillments) *SettlementTransfers { - allOFs := make([]*OrderFulfillment, 0, len(f.AskOFs)+len(f.BidOFs)) - allOFs = append(allOFs, f.AskOFs...) - allOFs = append(allOFs, f.BidOFs...) - - indexedFees := newIndexedAddrAmts() - rv := &SettlementTransfers{ - OrderTransfers: make([]*Transfer, 0, len(allOFs)*2), - } - - for _, of := range allOFs { - rv.OrderTransfers = append(rv.OrderTransfers, GetAssetTransfer2(of), GetPriceTransfer2(of)) - if !of.FeesToPay.IsZero() { - // Using NewCoins in here as a last-ditch negative amount panic check. - fees := sdk.NewCoins(of.FeesToPay...) - indexedFees.add(of.GetOwner(), fees...) - } - } - - rv.FeeInputs = indexedFees.getAsInputs() - - return rv -} - -// GetAssetTransfer2 gets the inputs and outputs to facilitate the transfers of assets for this order fulfillment. -// Assumes that the fulfillment has passed Validate2 already. -// Panics if any amounts are negative or if it's neither a bid nor ask order. -func GetAssetTransfer2(f *OrderFulfillment) *Transfer { - indexedSplits := newIndexedAddrAmts() - for _, split := range f.Splits { - indexedSplits.add(split.Order.GetOwner(), split.Assets) - } - - // Using NewCoins in here (instead of Coins{...}) as a last-ditch negative amount panic check. - if f.IsAskOrder() { - return &Transfer{ - Inputs: []banktypes.Input{{Address: f.GetOwner(), Coins: sdk.NewCoins(f.GetAssetsFilled())}}, - Outputs: indexedSplits.getAsOutputs(), - } - } - if f.IsBidOrder() { - return &Transfer{ - Inputs: indexedSplits.getAsInputs(), - Outputs: []banktypes.Output{{Address: f.GetOwner(), Coins: sdk.NewCoins(f.GetAssetsFilled())}}, - } - } - - // panicking in here if there's an error since it really should have happened earlier anyway. - panic(fmt.Errorf("unknown order type %T", f.Order.GetOrder())) -} - -// GetPriceTransfer2 gets the inputs and outputs to facilitate the transfers for the price of this order fulfillment. -// Assumes that the fulfillment has passed Validate2 already. -// Panics if any amounts are negative or if it's neither a bid nor ask order. -func GetPriceTransfer2(f *OrderFulfillment) *Transfer { - indexedSplits := newIndexedAddrAmts() - for _, split := range f.Splits { - indexedSplits.add(split.Order.GetOwner(), split.Price) - } - - // Using NewCoins in here (instead of Coins{...}) as a last-ditch negative amount panic check. - if f.IsAskOrder() { - return &Transfer{ - Inputs: indexedSplits.getAsInputs(), - Outputs: []banktypes.Output{{Address: f.GetOwner(), Coins: sdk.NewCoins(f.GetPriceApplied())}}, - } - } - if f.IsBidOrder() { - return &Transfer{ - Inputs: []banktypes.Input{{Address: f.GetOwner(), Coins: sdk.NewCoins(f.GetPriceApplied())}}, - Outputs: indexedSplits.getAsOutputs(), - } - } - - // panicking in here if there's an error since it really should have happened earlier anyway. - panic(fmt.Errorf("unknown order type %T", f.Order.GetOrder())) -} diff --git a/x/exchange/fulfillment_test.go b/x/exchange/fulfillment_test.go index 3bd100b13c..c39087f035 100644 --- a/x/exchange/fulfillment_test.go +++ b/x/exchange/fulfillment_test.go @@ -37,167 +37,82 @@ func copySlice[T any](vals []T, copier func(T) T) []T { return rv } -// copyOrderSplit creates a copy of this order split. -// Unlike the other copiers in here, the Order is not deep copied, it will be the same reference. -func copyOrderSplit(split *OrderSplit) *OrderSplit { - if split == nil { - return nil - } - - return &OrderSplit{ - // just copying the reference here to prevent infinite recursion. - Order: split.Order, - Assets: copyCoin(split.Assets), - Price: copyCoin(split.Price), - } -} - -// copyOrderSplits copies a slice of order splits. -func copyOrderSplits(splits []*OrderSplit) []*OrderSplit { - if splits == nil { - return nil - } - - rv := make([]*OrderSplit, len(splits)) - for i, split := range splits { - rv[i] = copyOrderSplit(split) - } - return rv -} - // copyDistribution copies a distribution. -func copyDistribution(dist *Distribution) *Distribution { +func copyDistribution(dist *distribution) *distribution { if dist == nil { return nil } - return &Distribution{ + return &distribution{ Address: dist.Address, Amount: copySDKInt(dist.Amount), } } // copyDistributions copies a slice of distributions. -func copyDistributions(dists []*Distribution) []*Distribution { +func copyDistributions(dists []*distribution) []*distribution { return copySlice(dists, copyDistribution) } // copyOrderFulfillment returns a deep copy of an order fulfillment. -func copyOrderFulfillment(f *OrderFulfillment) *OrderFulfillment { +func copyOrderFulfillment(f *orderFulfillment) *orderFulfillment { if f == nil { return nil } - return &OrderFulfillment{ + return &orderFulfillment{ Order: copyOrder(f.Order), - Splits: copyOrderSplits(f.Splits), AssetDists: copyDistributions(f.AssetDists), PriceDists: copyDistributions(f.PriceDists), AssetsFilledAmt: copySDKInt(f.AssetsFilledAmt), AssetsUnfilledAmt: copySDKInt(f.AssetsUnfilledAmt), PriceAppliedAmt: copySDKInt(f.PriceAppliedAmt), PriceLeftAmt: copySDKInt(f.PriceLeftAmt), - IsFinalized: f.IsFinalized, FeesToPay: copyCoins(f.FeesToPay), - OrderFeesLeft: copyCoins(f.OrderFeesLeft), - PriceFilledAmt: copySDKInt(f.PriceFilledAmt), - PriceUnfilledAmt: copySDKInt(f.PriceUnfilledAmt), } } // copyOrderFulfillments returns a deep copy of a slice of order fulfillments. -func copyOrderFulfillments(fs []*OrderFulfillment) []*OrderFulfillment { +func copyOrderFulfillments(fs []*orderFulfillment) []*orderFulfillment { return copySlice(fs, copyOrderFulfillment) } -// copyInput returns a deep copy of a bank input. -func copyInput(input banktypes.Input) banktypes.Input { - return banktypes.Input{ - Address: input.Address, - Coins: copyCoins(input.Coins), - } -} - -// copyInputs returns a deep copy of a slice of bank inputs. -func copyInputs(inputs []banktypes.Input) []banktypes.Input { - return copySlice(inputs, copyInput) -} - -// copyOutput returns a deep copy of a bank output. -func copyOutput(output banktypes.Output) banktypes.Output { - return banktypes.Output{ - Address: output.Address, - Coins: copyCoins(output.Coins), - } -} - -// copyOutputs returns a deep copy of a slice of bank outputs. -func copyOutputs(outputs []banktypes.Output) []banktypes.Output { - return copySlice(outputs, copyOutput) -} - -// copyTransfer returns a deep copy of a transfer. -func copyTransfer(t *Transfer) *Transfer { - if t == nil { +// copyIndexedAddrAmts creates a deep copy of an indexedAddrAmts. +func copyIndexedAddrAmts(orig *indexedAddrAmts) *indexedAddrAmts { + if orig == nil { return nil } - return &Transfer{ - Inputs: copyInputs(t.Inputs), - Outputs: copyOutputs(t.Outputs), - } -} - -// copyTransfers returns a deep copy of a slice of transfers. -func copyTransfers(ts []*Transfer) []*Transfer { - return copySlice(ts, copyTransfer) -} -// copyFilledOrder returns a deep copy of a filled order. -func copyFilledOrder(f *FilledOrder) *FilledOrder { - if f == nil { - return nil - } - return &FilledOrder{ - order: copyOrder(f.order), - actualPrice: copyCoin(f.actualPrice), - actualFees: copyCoins(f.actualFees), + rv := &indexedAddrAmts{ + addrs: nil, + amts: nil, + indexes: nil, } -} - -// copyFilledOrders returns a deep copy of a slice of filled order. -func copyFilledOrders(fs []*FilledOrder) []*FilledOrder { - return copySlice(fs, copyFilledOrder) -} -// orderSplitString is similar to %v except with easier to understand Coin and Int entries. -func orderSplitString(s *OrderSplit) string { - if s == nil { - return "nil" + if orig.addrs != nil { + rv.addrs = make([]string, 0, len(orig.addrs)) + rv.addrs = append(rv.addrs, orig.addrs...) } - fields := []string{ - // Just using superficial info for the order to prevent infinite loops. - fmt.Sprintf("Order:{OrderID:%d,OrderType:%s,...}", s.Order.GetOrderID(), s.Order.GetOrderType()), - fmt.Sprintf("Assets:%q", s.Assets), - fmt.Sprintf("Price:%q", s.Price), + if orig.amts != nil { + rv.amts = make([]sdk.Coins, len(orig.amts)) + for i, amt := range orig.amts { + rv.amts[i] = copyCoins(amt) + } } - return fmt.Sprintf("{%s}", strings.Join(fields, ", ")) -} -// orderSplitsString is similar to %v except with easier to understand Coin and Int entries. -func orderSplitsString(splits []*OrderSplit) string { - if splits == nil { - return "nil" - } - vals := make([]string, len(splits)) - for i, s := range splits { - vals[i] = orderSplitString(s) + if orig.indexes != nil { + rv.indexes = make(map[string]int, len(orig.indexes)) + for k, v := range orig.indexes { + rv.indexes[k] = v + } } - return fmt.Sprintf("[%s]", strings.Join(vals, ", ")) + + return rv } // distributionString is similar to %v except with easier to understand Int entries. -func distributionString(dist *Distribution) string { +func distributionString(dist *distribution) string { if dist == nil { return "nil" } @@ -205,7 +120,7 @@ func distributionString(dist *Distribution) string { } // distributionsString is similar to %v except with easier to understand Int entries. -func distributionsString(dists []*Distribution) string { +func distributionsString(dists []*distribution) string { if dists == nil { return "nil" } @@ -217,49 +132,35 @@ func distributionsString(dists []*Distribution) string { } // orderFulfillmentString is similar to %v except with easier to understand Coin and Int entries. -func orderFulfillmentString(f *OrderFulfillment) string { +func orderFulfillmentString(f *orderFulfillment) string { if f == nil { return "nil" } fields := []string{ fmt.Sprintf("Order:%s", orderString(f.Order)), - fmt.Sprintf("Splits:%s", orderSplitsString(f.Splits)), fmt.Sprintf("AssetDists:%s", distributionsString(f.AssetDists)), fmt.Sprintf("PriceDists:%s", distributionsString(f.PriceDists)), fmt.Sprintf("AssetsFilledAmt:%s", f.AssetsFilledAmt), fmt.Sprintf("AssetsUnfilledAmt:%s", f.AssetsUnfilledAmt), fmt.Sprintf("PriceAppliedAmt:%s", f.PriceAppliedAmt), fmt.Sprintf("PriceLeftAmt:%s", f.PriceLeftAmt), - fmt.Sprintf("IsFinalized:%t", f.IsFinalized), fmt.Sprintf("FeesToPay:%s", coinsString(f.FeesToPay)), - fmt.Sprintf("OrderFeesLeft:%s", coinsString(f.OrderFeesLeft)), - fmt.Sprintf("PriceFilledAmt:%s", f.PriceFilledAmt), - fmt.Sprintf("PriceUnfilledAmt:%s", f.PriceUnfilledAmt), } return fmt.Sprintf("{%s}", strings.Join(fields, ", ")) } -// transfersStringsLines creates a string for each transfer. -func transfersStringsLines(ts []*Transfer) []string { - if ts == nil { - return nil - } - rv := make([]string, len(ts)) - for i, t := range ts { - rv[i] = transferString(t) +// orderFulfillmentsString is similar to %v except with easier to understand Coin entries. +func orderFulfillmentsString(ofs []*orderFulfillment) string { + if ofs == nil { + return "nil" } - return rv -} -// transferString is similar to %v except with easier to understand Coin entries. -func transferString(t *Transfer) string { - if t == nil { - return "nil" + vals := make([]string, len(ofs)) + for i, f := range ofs { + vals[i] = orderFulfillmentString(f) } - inputs := bankInputsString(t.Inputs) - outputs := bankOutputsString(t.Outputs) - return fmt.Sprintf("T{Inputs:%s, Outputs: %s}", inputs, outputs) + return fmt.Sprintf("[%s]", strings.Join(vals, ", ")) } // bankInputString is similar to %v except with easier to understand Coin entries. @@ -296,10 +197,70 @@ func bankOutputsString(outs []banktypes.Output) string { return fmt.Sprintf("[%s]", strings.Join(vals, ", ")) } +// transferString is similar to %v except with easier to understand Coin entries. +func transferString(t *Transfer) string { + if t == nil { + return "nil" + } + inputs := bankInputsString(t.Inputs) + outputs := bankOutputsString(t.Outputs) + return fmt.Sprintf("T{Inputs:%s, Outputs: %s}", inputs, outputs) +} + +// transfersStringsLines creates a string for each transfer. +func transfersStringsLines(ts []*Transfer) []string { + if ts == nil { + return nil + } + rv := make([]string, len(ts)) + for i, t := range ts { + rv[i] = transferString(t) + } + return rv +} + +// String converts a indexedAddrAmtsString to a string. +// This is mostly because test failure output of sdk.Coin and sdk.Coins is impossible to understand. +func indexedAddrAmtsString(i *indexedAddrAmts) string { + if i == nil { + return "nil" + } + + addrs := "nil" + if i.addrs != nil { + addrsVals := make([]string, len(i.addrs)) + for j, addr := range i.addrs { + addrsVals[j] = fmt.Sprintf("%q", addr) + } + addrs = fmt.Sprintf("%T{%s}", i.addrs, strings.Join(addrsVals, ", ")) + } + + amts := "nil" + if i.amts != nil { + amtsVals := make([]string, len(i.amts)) + for j, amt := range i.amts { + amtsVals[j] = fmt.Sprintf("%q", amt) + } + amts = fmt.Sprintf("[]%T{%s}", i.amts, strings.Join(amtsVals, ", ")) + } + + indexes := "nil" + if i.indexes != nil { + indexVals := make([]string, 0, len(i.indexes)) + for k, v := range i.indexes { + indexVals = append(indexVals, fmt.Sprintf("%q: %d", k, v)) + } + sort.Strings(indexVals) + indexes = fmt.Sprintf("%T{%s}", i.indexes, strings.Join(indexVals, ", ")) + } + + return fmt.Sprintf("%T{addrs:%s, amts:%s, indexes:%s}", i, addrs, amts, indexes) +} + // assertEqualOrderFulfillments asserts that the two order fulfillments are equal. // Returns true if equal. // If not equal, and neither are nil, equality on each field is also asserted in order to help identify the problem. -func assertEqualOrderFulfillments(t *testing.T, expected, actual *OrderFulfillment, message string, args ...interface{}) bool { +func assertEqualOrderFulfillments(t *testing.T, expected, actual *orderFulfillment, message string, args ...interface{}) bool { t.Helper() if assert.Equalf(t, expected, actual, message, args...) { return true @@ -318,19 +279,14 @@ func assertEqualOrderFulfillments(t *testing.T, expected, actual *OrderFulfillme // Assert equality on each individual field so that we can more easily find the problem. // If any of the Ints fail with a complaint about Int.abs = (big.nat) vs {}, use ZeroAmtAfterSub for the expected. - assert.Equalf(t, expected.Order, actual.Order, msg("OrderFulfillment.Order"), args...) - assert.Equalf(t, expected.Splits, actual.Splits, msg("OrderFulfillment.Splits"), args...) - assert.Equalf(t, expected.AssetDists, actual.AssetDists, msg("OrderFulfillment.AssetDists"), args...) - assert.Equalf(t, expected.PriceDists, actual.PriceDists, msg("OrderFulfillment.PriceDists"), args...) - assert.Equalf(t, expected.AssetsFilledAmt, actual.AssetsFilledAmt, msg("OrderFulfillment.AssetsFilledAmt"), args...) - assert.Equalf(t, expected.AssetsUnfilledAmt, actual.AssetsUnfilledAmt, msg("OrderFulfillment.AssetsUnfilledAmt"), args...) - assert.Equalf(t, expected.PriceAppliedAmt, actual.PriceAppliedAmt, msg("OrderFulfillment.PriceAppliedAmt"), args...) - assert.Equalf(t, expected.PriceLeftAmt, actual.PriceLeftAmt, msg("OrderFulfillment.PriceLeftAmt"), args...) - assert.Equalf(t, expected.IsFinalized, actual.IsFinalized, msg("OrderFulfillment.IsFinalized"), args...) - assert.Equalf(t, expected.FeesToPay, actual.FeesToPay, msg("OrderFulfillment.FeesToPay"), args...) - assert.Equalf(t, expected.OrderFeesLeft, actual.OrderFeesLeft, msg("OrderFulfillment.OrderFeesLeft"), args...) - assert.Equalf(t, expected.PriceFilledAmt, actual.PriceFilledAmt, msg("OrderFulfillment.PriceFilledAmt"), args...) - assert.Equalf(t, expected.PriceUnfilledAmt, actual.PriceUnfilledAmt, msg("OrderFulfillment.PriceUnfilledAmt"), args...) + assert.Equalf(t, expected.Order, actual.Order, msg("orderFulfillment.Order"), args...) + assert.Equalf(t, expected.AssetDists, actual.AssetDists, msg("orderFulfillment.AssetDists"), args...) + assert.Equalf(t, expected.PriceDists, actual.PriceDists, msg("orderFulfillment.PriceDists"), args...) + assert.Equalf(t, expected.AssetsFilledAmt, actual.AssetsFilledAmt, msg("orderFulfillment.AssetsFilledAmt"), args...) + assert.Equalf(t, expected.AssetsUnfilledAmt, actual.AssetsUnfilledAmt, msg("orderFulfillment.AssetsUnfilledAmt"), args...) + assert.Equalf(t, expected.PriceAppliedAmt, actual.PriceAppliedAmt, msg("orderFulfillment.PriceAppliedAmt"), args...) + assert.Equalf(t, expected.PriceLeftAmt, actual.PriceLeftAmt, msg("orderFulfillment.PriceLeftAmt"), args...) + assert.Equalf(t, expected.FeesToPay, actual.FeesToPay, msg("orderFulfillment.FeesToPay"), args...) t.Logf(" Actual: %s", orderFulfillmentString(actual)) t.Logf("Expected: %s", orderFulfillmentString(expected)) return false @@ -339,7 +295,7 @@ func assertEqualOrderFulfillments(t *testing.T, expected, actual *OrderFulfillme // assertEqualOrderFulfillmentSlices asserts that the two order fulfillments are equal. // Returns true if equal. // If not equal, and neither are nil, equality on each field is also asserted in order to help identify the problem. -func assertEqualOrderFulfillmentSlices(t *testing.T, expected, actual []*OrderFulfillment, message string, args ...interface{}) bool { +func assertEqualOrderFulfillmentSlices(t *testing.T, expected, actual []*orderFulfillment, message string, args ...interface{}) bool { t.Helper() if assert.Equalf(t, expected, actual, message, args...) { return true @@ -381,7 +337,7 @@ func assertEqualOrderFulfillmentSlices(t *testing.T, expected, actual []*OrderFu actStrVals[i] = orderFulfillmentString(act) } actStrs := strings.Join(actStrVals, "\n") - if !assert.Equalf(t, expStrs, actStrs, msg("OrderFulfillment strings"), args...) { + if !assert.Equalf(t, expStrs, actStrs, msg("orderFulfillment strings"), args...) { // Wooo, should have actionable info in the failure, so we can be done. return false } @@ -395,2412 +351,244 @@ func assertEqualOrderFulfillmentSlices(t *testing.T, expected, actual []*OrderFu return false } -func TestNewOrderFulfillment(t *testing.T) { +func TestBuildSettlement(t *testing.T) { + assetDenom, priceDenom := "apple", "peach" + feeDenoms := []string{"fig", "grape"} + feeCoins := func(tracer string, amts []int64) sdk.Coins { + if len(amts) == 0 { + return nil + } + if len(amts) > len(feeDenoms) { + t.Fatalf("cannot create %s with more than %d fees %v", tracer, len(feeDenoms), amts) + } + var rv sdk.Coins + for i, amt := range amts { + rv = rv.Add(sdk.NewInt64Coin(feeDenoms[i], amt)) + } + return rv + } + askOrder := func(orderID uint64, assets, price int64, allowPartial bool, fees ...int64) *Order { + if len(fees) > 1 { + t.Fatalf("cannot create ask order %d with more than 1 fees %v", orderID, fees) + } + var fee *sdk.Coin + if fc := feeCoins("", fees); !fc.IsZero() { + fee = &fc[0] + } + return NewOrder(orderID).WithAsk(&AskOrder{ + Seller: fmt.Sprintf("seller%d", orderID), + Assets: sdk.NewInt64Coin(assetDenom, assets), + Price: sdk.NewInt64Coin(priceDenom, price), + SellerSettlementFlatFee: fee, + AllowPartial: allowPartial, + }) + } + bidOrder := func(orderID uint64, assets, price int64, allowPartial bool, fees ...int64) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + Buyer: fmt.Sprintf("buyer%d", orderID), + Assets: sdk.NewInt64Coin(assetDenom, assets), + Price: sdk.NewInt64Coin(priceDenom, price), + BuyerSettlementFees: feeCoins(fmt.Sprintf("bid order %d", orderID), fees), + AllowPartial: allowPartial, + }) + } + ratio := func(price, fee int64) func(denom string) (*FeeRatio, error) { + return func(denom string) (*FeeRatio, error) { + return &FeeRatio{Price: sdk.NewInt64Coin(priceDenom, price), Fee: sdk.NewInt64Coin(feeDenoms[0], fee)}, nil + } + } + filled := func(order *Order, price int64, fees ...int64) *FilledOrder { + return NewFilledOrder(order, + sdk.NewInt64Coin(priceDenom, price), + feeCoins(fmt.Sprintf("filled order %d", order), fees)) + } + assetsInput := func(orderID uint64, amount int64) banktypes.Input { + return banktypes.Input{ + Address: fmt.Sprintf("seller%d", orderID), + Coins: sdk.NewCoins(sdk.NewInt64Coin(assetDenom, amount)), + } + } + assetsOutput := func(orderID uint64, amount int64) banktypes.Output { + return banktypes.Output{ + Address: fmt.Sprintf("buyer%d", orderID), + Coins: sdk.NewCoins(sdk.NewInt64Coin(assetDenom, amount)), + } + } + priceInput := func(orderID uint64, amount int64) banktypes.Input { + return banktypes.Input{ + Address: fmt.Sprintf("buyer%d", orderID), + Coins: sdk.NewCoins(sdk.NewInt64Coin(priceDenom, amount)), + } + } + priceOutput := func(orderID uint64, amount int64) banktypes.Output { + return banktypes.Output{ + Address: fmt.Sprintf("seller%d", orderID), + Coins: sdk.NewCoins(sdk.NewInt64Coin(priceDenom, amount)), + } + } + feeInput := func(address string, amts ...int64) banktypes.Input { + return banktypes.Input{Address: address, Coins: feeCoins("bank input for "+address, amts)} + } + tests := []struct { - name string - order *Order - expected *OrderFulfillment - expPanic string + name string + askOrders []*Order + bidOrders []*Order + sellerFeeRatioLookup func(denom string) (*FeeRatio, error) + expSettlement *Settlement + expErr string }{ { - name: "nil sub-order", - order: NewOrder(1), - expPanic: nilSubTypeErr(1), + // error from validateCanSettle + name: "no ask orders", + askOrders: []*Order{}, + bidOrders: []*Order{bidOrder(3, 1, 10, false)}, + expErr: "no ask orders provided", }, { - name: "ask order", - order: NewOrder(2).WithAsk(&AskOrder{ - MarketId: 10, - Assets: sdk.NewInt64Coin("adolla", 92), - Price: sdk.NewInt64Coin("pdolla", 15), - }), - expected: &OrderFulfillment{ - Order: &Order{ - OrderId: 2, - Order: &Order_AskOrder{ - AskOrder: &AskOrder{ - MarketId: 10, - Assets: sdk.NewInt64Coin("adolla", 92), - Price: sdk.NewInt64Coin("pdolla", 15), - }, - }, - }, - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(92), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(15), - IsFinalized: false, - FeesToPay: nil, - OrderFeesLeft: nil, - PriceFilledAmt: sdkmath.ZeroInt(), - PriceUnfilledAmt: sdkmath.ZeroInt(), + name: "error from ratio lookup", + askOrders: []*Order{askOrder(3, 1, 10, false)}, + bidOrders: []*Order{bidOrder(4, 1, 10, false)}, + sellerFeeRatioLookup: func(denom string) (*FeeRatio, error) { + return nil, errors.New("this is a test error") }, + expErr: "this is a test error", }, { - name: "bid order", - order: NewOrder(3).WithBid(&BidOrder{ - MarketId: 11, - Assets: sdk.NewInt64Coin("adolla", 93), - Price: sdk.NewInt64Coin("pdolla", 16), - }), - expected: &OrderFulfillment{ - Order: &Order{ - OrderId: 3, - Order: &Order_BidOrder{ - BidOrder: &BidOrder{ - MarketId: 11, - Assets: sdk.NewInt64Coin("adolla", 93), - Price: sdk.NewInt64Coin("pdolla", 16), - }, - }, - }, - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(93), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(16), - IsFinalized: false, - FeesToPay: nil, - OrderFeesLeft: nil, - PriceFilledAmt: sdkmath.ZeroInt(), - PriceUnfilledAmt: sdkmath.ZeroInt(), + name: "error from setFeesToPay", + askOrders: []*Order{askOrder(3, 1, 10, false)}, + bidOrders: []*Order{bidOrder(4, 1, 10, false)}, + sellerFeeRatioLookup: func(denom string) (*FeeRatio, error) { + return &FeeRatio{Price: sdk.NewInt64Coin("prune", 10), Fee: sdk.NewInt64Coin("fig", 1)}, nil }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual *OrderFulfillment - defer func() { - if t.Failed() { - t.Logf(" Actual: %s", orderFulfillmentString(actual)) - t.Logf("Expected: %s", orderFulfillmentString(tc.expected)) - } - }() - - testFunc := func() { - actual = NewOrderFulfillment(tc.order) - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "NewOrderFulfillment") - assert.Equal(t, tc.expected, actual, "NewOrderFulfillment result") - }) - } -} - -func TestNewOrderFulfillments(t *testing.T) { - assetCoin := func(amount int64) sdk.Coin { - return sdk.NewInt64Coin("anise", amount) - } - priceCoin := func(amount int64) sdk.Coin { - return sdk.NewInt64Coin("paprika", amount) - } - feeCoin := func(amount int64) *sdk.Coin { - rv := sdk.NewInt64Coin("fennel", amount) - return &rv - } - - askOrders := make([]*Order, 4) // ids 1, 2, 3, 4 - for j := range askOrders { - i := int64(j) + 1 - order := &AskOrder{ - MarketId: uint32(90 + i), - Seller: fmt.Sprintf("seller-%d", i), - Assets: assetCoin(1000*i + 100*i + 10*i + i), - Price: priceCoin(100*i + 10*i + i), - } - if j%2 == 0 { - order.SellerSettlementFlatFee = feeCoin(10*i + i) - } - if j >= 2 { - order.AllowPartial = true - } - askOrders[j] = NewOrder(uint64(i)).WithAsk(order) - } - - bidOrders := make([]*Order, 4) // ids 5, 6, 7, 8 - for j := range bidOrders { - i := int64(j + 5) - order := &BidOrder{ - MarketId: uint32(90 + i), - Buyer: fmt.Sprintf("buyer-%d", i), - Assets: assetCoin(1000*i + 100*i + 10*i + i), - Price: priceCoin(100*i + 10*i + i), - } - switch j { - case 0: - order.BuyerSettlementFees = sdk.Coins{*feeCoin(10*i + i)} - case 2: - order.BuyerSettlementFees = sdk.Coins{ - *feeCoin(10*i + i), - sdk.NewInt64Coin("garlic", 10000*i+1000*i+100*i+10*i+i), - } - } - if j >= 2 { - order.AllowPartial = true - } - bidOrders[j] = NewOrder(uint64(i)).WithBid(order) - } - - tests := []struct { - name string - orders []*Order - expected []*OrderFulfillment - }{ - { - name: "nil orders", - orders: nil, - expected: []*OrderFulfillment{}, + expErr: "failed calculate ratio fee for ask order 3: cannot apply ratio 10prune:1fig to price 10peach: incorrect price denom", }, { - name: "empty orders", - orders: []*Order{}, - expected: []*OrderFulfillment{}, + name: "one ask, three bids: last bid not used", + askOrders: []*Order{askOrder(3, 10, 20, false)}, + bidOrders: []*Order{ + bidOrder(4, 7, 14, false), + bidOrder(5, 3, 6, false), + bidOrder(6, 1, 2, false), + }, + expErr: "bid order 6 (at index 2) has no assets filled", }, { - name: "1 ask order", - orders: []*Order{askOrders[0]}, - expected: []*OrderFulfillment{NewOrderFulfillment(askOrders[0])}, + name: "three asks, one bids: last ask not used", + askOrders: []*Order{ + askOrder(11, 7, 14, false), + askOrder(12, 3, 14, false), + askOrder(13, 1, 14, false), + }, + bidOrders: []*Order{bidOrder(14, 10, 20, false)}, + expErr: "ask order 13 (at index 2) has no assets filled", }, { - name: "1 bid order", - orders: []*Order{bidOrders[0]}, - expected: []*OrderFulfillment{NewOrderFulfillment(bidOrders[0])}, + name: "two asks, two bids: same assets total, total bid price not enough", + askOrders: []*Order{ + askOrder(1, 10, 25, false), + askOrder(2, 10, 15, false), + }, + bidOrders: []*Order{ + bidOrder(8, 10, 20, false), + bidOrder(9, 10, 19, false), + }, + expErr: "total ask price \"40peach\" is greater than total bid price \"39peach\"", }, { - name: "4 ask orders", - orders: []*Order{askOrders[0], askOrders[1], askOrders[2], askOrders[3]}, - expected: []*OrderFulfillment{ - NewOrderFulfillment(askOrders[0]), - NewOrderFulfillment(askOrders[1]), - NewOrderFulfillment(askOrders[2]), - NewOrderFulfillment(askOrders[3]), + name: "two asks, two bids: ask partial, total bid price not enough", + askOrders: []*Order{ + askOrder(1, 10, 20, false), + askOrder(2, 10, 20, true), + }, + bidOrders: []*Order{ + bidOrder(8, 10, 20, false), + bidOrder(9, 9, 17, false), }, + expErr: "total ask price \"38peach\" is greater than total bid price \"37peach\"", }, { - name: "4 bid orders", - orders: []*Order{bidOrders[0], bidOrders[1], bidOrders[2], bidOrders[3]}, - expected: []*OrderFulfillment{ - NewOrderFulfillment(bidOrders[0]), - NewOrderFulfillment(bidOrders[1]), - NewOrderFulfillment(bidOrders[2]), - NewOrderFulfillment(bidOrders[3]), + name: "two asks, two bids: bid partial, total bid price not enough", + askOrders: []*Order{ + askOrder(1, 10, 25, false), + askOrder(2, 10, 15, false), + }, + bidOrders: []*Order{ + bidOrder(8, 10, 19, false), + bidOrder(9, 11, 22, true), }, + expErr: "total ask price \"40peach\" is greater than total bid price \"39peach\"", }, { - name: "1 bid 1 ask", - orders: []*Order{askOrders[1], bidOrders[2]}, - expected: []*OrderFulfillment{ - NewOrderFulfillment(askOrders[1]), - NewOrderFulfillment(bidOrders[2]), + name: "one ask, one bid: both fully filled", + askOrders: []*Order{askOrder(52, 10, 100, false, 2)}, + bidOrders: []*Order{bidOrder(11, 10, 105, false, 3, 4)}, + sellerFeeRatioLookup: ratio(4, 1), + expSettlement: &Settlement{ + Transfers: []*Transfer{ + {Inputs: []banktypes.Input{assetsInput(52, 10)}, Outputs: []banktypes.Output{assetsOutput(11, 10)}}, + {Inputs: []banktypes.Input{priceInput(11, 105)}, Outputs: []banktypes.Output{priceOutput(52, 105)}}, + }, + FeeInputs: []banktypes.Input{ + feeInput("seller52", 29), + feeInput("buyer11", 3, 4), + }, + FullyFilledOrders: []*FilledOrder{ + filled(askOrder(52, 10, 100, false, 2), 105, 29), + filled(bidOrder(11, 10, 105, false, 3, 4), 105, 3, 4), + }, }, }, { - name: "1 ask 1 bid", - orders: []*Order{bidOrders[1], askOrders[2]}, - expected: []*OrderFulfillment{ - NewOrderFulfillment(bidOrders[1]), - NewOrderFulfillment(askOrders[2]), + name: "one ask, one bid: ask partially filled", + askOrders: []*Order{askOrder(99, 10, 100, true)}, + bidOrders: []*Order{bidOrder(15, 9, 90, false)}, + expSettlement: &Settlement{ + Transfers: []*Transfer{ + {Inputs: []banktypes.Input{assetsInput(99, 9)}, Outputs: []banktypes.Output{assetsOutput(15, 9)}}, + {Inputs: []banktypes.Input{priceInput(15, 90)}, Outputs: []banktypes.Output{priceOutput(99, 90)}}, + }, + FullyFilledOrders: []*FilledOrder{filled(bidOrder(15, 9, 90, false), 90)}, + PartialOrderFilled: filled(askOrder(99, 9, 90, true), 90), + PartialOrderLeft: askOrder(99, 1, 10, true), }, }, { - name: "4 asks 4 bids", - orders: []*Order{ - askOrders[0], askOrders[1], askOrders[2], askOrders[3], - bidOrders[3], bidOrders[2], bidOrders[1], bidOrders[0], - }, - expected: []*OrderFulfillment{ - NewOrderFulfillment(askOrders[0]), - NewOrderFulfillment(askOrders[1]), - NewOrderFulfillment(askOrders[2]), - NewOrderFulfillment(askOrders[3]), - NewOrderFulfillment(bidOrders[3]), - NewOrderFulfillment(bidOrders[2]), - NewOrderFulfillment(bidOrders[1]), - NewOrderFulfillment(bidOrders[0]), - }, + name: "one ask, one bid: ask partially filled, not allowed", + askOrders: []*Order{askOrder(99, 10, 100, false)}, + bidOrders: []*Order{bidOrder(15, 9, 90, false)}, + expErr: "cannot split ask order 99 having assets \"10apple\" at \"9apple\": order does not allow partial fulfillment", }, { - name: "4 bids 4 asks", - orders: []*Order{ - bidOrders[0], bidOrders[1], bidOrders[2], bidOrders[3], - askOrders[3], askOrders[2], askOrders[1], askOrders[0], - }, - expected: []*OrderFulfillment{ - NewOrderFulfillment(bidOrders[0]), - NewOrderFulfillment(bidOrders[1]), - NewOrderFulfillment(bidOrders[2]), - NewOrderFulfillment(bidOrders[3]), - NewOrderFulfillment(askOrders[3]), - NewOrderFulfillment(askOrders[2]), - NewOrderFulfillment(askOrders[1]), - NewOrderFulfillment(askOrders[0]), + name: "one ask, one bid: bid partially filled", + askOrders: []*Order{askOrder(8, 9, 85, false, 2)}, + bidOrders: []*Order{bidOrder(12, 10, 100, true)}, + expSettlement: &Settlement{ + Transfers: []*Transfer{ + {Inputs: []banktypes.Input{assetsInput(8, 9)}, Outputs: []banktypes.Output{assetsOutput(12, 9)}}, + {Inputs: []banktypes.Input{priceInput(12, 90)}, Outputs: []banktypes.Output{priceOutput(8, 90)}}, + }, + FeeInputs: []banktypes.Input{feeInput("seller8", 2)}, + FullyFilledOrders: []*FilledOrder{filled(askOrder(8, 9, 85, false, 2), 90, 2)}, + PartialOrderFilled: filled(bidOrder(12, 9, 90, true), 90), + PartialOrderLeft: bidOrder(12, 1, 10, true), }, }, { - name: "interweaved 4 asks 4 bids", - orders: []*Order{ - bidOrders[3], askOrders[0], askOrders[3], bidOrders[1], - bidOrders[0], askOrders[1], bidOrders[2], askOrders[2], - }, - expected: []*OrderFulfillment{ - NewOrderFulfillment(bidOrders[3]), - NewOrderFulfillment(askOrders[0]), - NewOrderFulfillment(askOrders[3]), - NewOrderFulfillment(bidOrders[1]), - NewOrderFulfillment(bidOrders[0]), - NewOrderFulfillment(askOrders[1]), - NewOrderFulfillment(bidOrders[2]), - NewOrderFulfillment(askOrders[2]), - }, + name: "one ask, one bid: bid partially filled, not allowed", + askOrders: []*Order{askOrder(8, 9, 85, false, 2)}, + bidOrders: []*Order{bidOrder(12, 10, 100, false)}, + expErr: "cannot split bid order 12 having assets \"10apple\" at \"9apple\": order does not allow partial fulfillment", }, { - name: "duplicated entries", - orders: []*Order{ - askOrders[3], bidOrders[2], askOrders[3], bidOrders[2], - }, - expected: []*OrderFulfillment{ - NewOrderFulfillment(askOrders[3]), - NewOrderFulfillment(bidOrders[2]), - NewOrderFulfillment(askOrders[3]), - NewOrderFulfillment(bidOrders[2]), - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual []*OrderFulfillment - testFunc := func() { - actual = NewOrderFulfillments(tc.orders) - } - require.NotPanics(t, testFunc, "NewOrderFulfillments") - assertEqualOrderFulfillmentSlices(t, tc.expected, actual, "NewOrderFulfillments result") - }) - } -} - -func TestOrderFulfillment_AssetCoin(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - bigCoin := func(amount, denom string) sdk.Coin { - amt := newInt(t, amount) - return sdk.Coin{Denom: denom, Amount: amt} - } - askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - Assets: assets, - Price: price, - }) - } - bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Assets: assets, - Price: price, - }) - } - - tests := []struct { - name string - receiver OrderFulfillment - amt sdkmath.Int - expected sdk.Coin - expPanic string - }{ - { - name: "nil order", - receiver: OrderFulfillment{Order: nil}, - amt: sdkmath.NewInt(0), - expPanic: "runtime error: invalid memory address or nil pointer dereference", - }, - { - name: "nil inside order", - receiver: OrderFulfillment{Order: NewOrder(1)}, - amt: sdkmath.NewInt(0), - expPanic: nilSubTypeErr(1), - }, - { - name: "unknown inside order", - receiver: OrderFulfillment{Order: newUnknownOrder(2)}, - amt: sdkmath.NewInt(0), - expPanic: unknownSubTypeErr(2), - }, - { - name: "ask order", - receiver: OrderFulfillment{Order: askOrder(3, coin(4, "apple"), coin(5, "plum"))}, - amt: sdkmath.NewInt(6), - expected: coin(6, "apple"), - }, - { - name: "ask order with negative assets", - receiver: OrderFulfillment{Order: askOrder(7, coin(-8, "apple"), coin(9, "plum"))}, - amt: sdkmath.NewInt(10), - expected: coin(10, "apple"), - }, - { - name: "ask order, negative amt", - receiver: OrderFulfillment{Order: askOrder(11, coin(12, "apple"), coin(13, "plum"))}, - amt: sdkmath.NewInt(-14), - expected: coin(-14, "apple"), - }, - { - name: "ask order with negative assets, negative amt", - receiver: OrderFulfillment{Order: askOrder(15, coin(-16, "apple"), coin(17, "plum"))}, - amt: sdkmath.NewInt(-18), - expected: coin(-18, "apple"), - }, - { - name: "ask order, big amt", - receiver: OrderFulfillment{Order: askOrder(19, coin(20, "apple"), coin(21, "plum"))}, - amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), - expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "apple"), - }, - { - name: "bid order", - receiver: OrderFulfillment{Order: bidOrder(3, coin(4, "apple"), coin(5, "plum"))}, - amt: sdkmath.NewInt(6), - expected: coin(6, "apple"), - }, - { - name: "bid order with negative assets", - receiver: OrderFulfillment{Order: bidOrder(7, coin(-8, "apple"), coin(9, "plum"))}, - amt: sdkmath.NewInt(10), - expected: coin(10, "apple"), - }, - { - name: "bid order, negative amt", - receiver: OrderFulfillment{Order: bidOrder(11, coin(12, "apple"), coin(13, "plum"))}, - amt: sdkmath.NewInt(-14), - expected: coin(-14, "apple"), - }, - { - name: "bid order with negative assets, negative amt", - receiver: OrderFulfillment{Order: bidOrder(15, coin(-16, "apple"), coin(17, "plum"))}, - amt: sdkmath.NewInt(-18), - expected: coin(-18, "apple"), - }, - { - name: "bid order, big amt", - receiver: OrderFulfillment{Order: bidOrder(19, coin(20, "apple"), coin(21, "plum"))}, - amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), - expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "apple"), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.receiver.assetCoin(tc.amt) - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "assetCoin(%s)", tc.amt) - assert.Equal(t, tc.expected.String(), actual.String(), "assetCoin(%s) result", tc.amt) - }) - } -} - -func TestOrderFulfillment_PriceCoin(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - bigCoin := func(amount, denom string) sdk.Coin { - amt := newInt(t, amount) - return sdk.Coin{Denom: denom, Amount: amt} - } - askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - Assets: assets, - Price: price, - }) - } - bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Assets: assets, - Price: price, - }) - } - - tests := []struct { - name string - receiver OrderFulfillment - amt sdkmath.Int - expected sdk.Coin - expPanic string - }{ - { - name: "nil order", - receiver: OrderFulfillment{Order: nil}, - amt: sdkmath.NewInt(0), - expPanic: "runtime error: invalid memory address or nil pointer dereference", - }, - { - name: "nil inside order", - receiver: OrderFulfillment{Order: NewOrder(1)}, - amt: sdkmath.NewInt(0), - expPanic: nilSubTypeErr(1), - }, - { - name: "unknown inside order", - receiver: OrderFulfillment{Order: newUnknownOrder(2)}, - amt: sdkmath.NewInt(0), - expPanic: unknownSubTypeErr(2), - }, - { - name: "ask order", - receiver: OrderFulfillment{Order: askOrder(3, coin(4, "apple"), coin(5, "plum"))}, - amt: sdkmath.NewInt(6), - expected: coin(6, "plum"), - }, - { - name: "ask order with negative assets", - receiver: OrderFulfillment{Order: askOrder(7, coin(-8, "apple"), coin(9, "plum"))}, - amt: sdkmath.NewInt(10), - expected: coin(10, "plum"), - }, - { - name: "ask order, negative amt", - receiver: OrderFulfillment{Order: askOrder(11, coin(12, "apple"), coin(13, "plum"))}, - amt: sdkmath.NewInt(-14), - expected: coin(-14, "plum"), - }, - { - name: "ask order with negative assets, negative amt", - receiver: OrderFulfillment{Order: askOrder(15, coin(-16, "apple"), coin(17, "plum"))}, - amt: sdkmath.NewInt(-18), - expected: coin(-18, "plum"), - }, - { - name: "ask order, big amt", - receiver: OrderFulfillment{Order: askOrder(19, coin(20, "apple"), coin(21, "plum"))}, - amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), - expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "plum"), - }, - { - name: "bid order", - receiver: OrderFulfillment{Order: bidOrder(3, coin(4, "apple"), coin(5, "plum"))}, - amt: sdkmath.NewInt(6), - expected: coin(6, "plum"), - }, - { - name: "bid order with negative assets", - receiver: OrderFulfillment{Order: bidOrder(7, coin(-8, "apple"), coin(9, "plum"))}, - amt: sdkmath.NewInt(10), - expected: coin(10, "plum"), - }, - { - name: "bid order, negative amt", - receiver: OrderFulfillment{Order: bidOrder(11, coin(12, "apple"), coin(13, "plum"))}, - amt: sdkmath.NewInt(-14), - expected: coin(-14, "plum"), - }, - { - name: "bid order with negative assets, negative amt", - receiver: OrderFulfillment{Order: bidOrder(15, coin(-16, "apple"), coin(17, "plum"))}, - amt: sdkmath.NewInt(-18), - expected: coin(-18, "plum"), - }, - { - name: "bid order, big amt", - receiver: OrderFulfillment{Order: bidOrder(19, coin(20, "apple"), coin(21, "plum"))}, - amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), - expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "plum"), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.receiver.priceCoin(tc.amt) - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "priceCoin(%s)", tc.amt) - assert.Equal(t, tc.expected.String(), actual.String(), "priceCoin(%s) result", tc.amt) - }) - } -} - -func TestOrderFulfillment_GetAssetsFilled(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(amt)} - } - askOrder := NewOrder(444).WithAsk(&AskOrder{Assets: coin(5555)}) - bidOrder := NewOrder(666).WithBid(&BidOrder{Assets: coin(7777)}) - - newOF := func(order *Order, amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: order, - AssetsFilledAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, - {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, - {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, - {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, - {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, - {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetAssetsFilled() - } - require.NotPanics(t, testFunc, "GetAssetsFilled()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetAssetsFilled() result") - }) - } -} - -func TestOrderFulfillment_GetAssetsUnfilled(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(amt)} - } - askOrder := NewOrder(444).WithAsk(&AskOrder{Assets: coin(5555)}) - bidOrder := NewOrder(666).WithBid(&BidOrder{Assets: coin(7777)}) - - newOF := func(order *Order, amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: order, - AssetsUnfilledAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, - {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, - {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, - {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, - {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, - {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetAssetsUnfilled() - } - require.NotPanics(t, testFunc, "GetAssetsUnfilled()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetAssetsUnfilled() result") - }) - } -} - -func TestOrderFulfillment_GetPriceApplied(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} - } - askOrder := NewOrder(444).WithAsk(&AskOrder{Price: coin(5555)}) - bidOrder := NewOrder(666).WithBid(&BidOrder{Price: coin(7777)}) - - newOF := func(order *Order, amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: order, - PriceAppliedAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, - {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, - {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, - {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, - {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, - {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetPriceApplied() - } - require.NotPanics(t, testFunc, "GetPriceApplied()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetPriceApplied() result") - }) - } -} - -func TestOrderFulfillment_GetPriceLeft(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} - } - askOrder := NewOrder(444).WithAsk(&AskOrder{Price: coin(5555)}) - bidOrder := NewOrder(666).WithBid(&BidOrder{Price: coin(7777)}) - - newOF := func(order *Order, amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: order, - PriceLeftAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, - {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, - {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, - {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, - {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, - {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetPriceLeft() - } - require.NotPanics(t, testFunc, "GetPriceLeft()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetPriceLeft() result") - }) - } -} - -func TestOrderFulfillment_GetPriceFilled(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} - } - askOrder := NewOrder(444).WithAsk(&AskOrder{Price: coin(5555)}) - bidOrder := NewOrder(666).WithBid(&BidOrder{Price: coin(7777)}) - - newOF := func(order *Order, amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: order, - PriceFilledAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, - {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, - {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, - {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, - {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, - {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetPriceFilled() - } - require.NotPanics(t, testFunc, "GetPriceFilled()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetPriceFilled() result") - }) - } -} - -func TestOrderFulfillment_GetPriceUnfilled(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} - } - askOrder := NewOrder(444).WithAsk(&AskOrder{Price: coin(5555)}) - bidOrder := NewOrder(666).WithBid(&BidOrder{Price: coin(7777)}) - - newOF := func(order *Order, amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: order, - PriceUnfilledAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, - {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, - {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, - {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, - {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, - {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetPriceUnfilled() - } - require.NotPanics(t, testFunc, "GetPriceUnfilled()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetPriceUnfilled() result") - }) - } -} - -func TestOrderFulfillment_IsFullyFilled(t *testing.T) { - newOF := func(amt int64) OrderFulfillment { - return OrderFulfillment{ - AssetsUnfilledAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp bool - }{ - {name: "positive assets unfilled", f: newOF(2), exp: false}, - {name: "zero assets unfilled", f: newOF(0), exp: true}, - {name: "negative assets unfilled", f: newOF(-3), exp: true}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual bool - testFunc := func() { - actual = tc.f.IsFullyFilled() - } - require.NotPanics(t, testFunc, "IsFullyFilled()") - assert.Equal(t, tc.exp, actual, "IsFullyFilled() result") - }) - } -} - -func TestOrderFulfillment_IsCompletelyUnfulfilled(t *testing.T) { - newOF := func(amt int64) OrderFulfillment { - return OrderFulfillment{ - AssetsFilledAmt: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp bool - }{ - {name: "positive assets filled", f: newOF(2), exp: false}, - {name: "zero assets filled", f: newOF(0), exp: true}, - {name: "negative assets filled", f: newOF(-3), exp: false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual bool - testFunc := func() { - actual = tc.f.IsCompletelyUnfulfilled() - } - require.NotPanics(t, testFunc, "IsCompletelyUnfulfilled()") - assert.Equal(t, tc.exp, actual, "IsCompletelyUnfulfilled() result") - }) - } -} - -func TestOrderFulfillment_GetOrderID(t *testing.T) { - newOF := func(orderID uint64) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(orderID), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp uint64 - }{ - {name: "zero", f: newOF(0), exp: 0}, - {name: "one", f: newOF(1), exp: 1}, - {name: "five", f: newOF(5), exp: 5}, - {name: "max uint32+1", f: newOF(4_294_967_296), exp: 4_294_967_296}, - {name: "max uint64", f: newOF(18_446_744_073_709_551_615), exp: 18_446_744_073_709_551_615}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual uint64 - testFunc := func() { - actual = tc.f.GetOrderID() - } - require.NotPanics(t, testFunc, "GetOrderID()") - assert.Equal(t, tc.exp, actual, "GetOrderID() result") - }) - } -} - -func TestOrderFulfillment_IsAskOrder(t *testing.T) { - tests := []struct { - name string - f OrderFulfillment - exp bool - }{ - {name: "ask", f: OrderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: true}, - {name: "bid", f: OrderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: false}, - {name: "nil", f: OrderFulfillment{Order: NewOrder(888)}, exp: false}, - {name: "unknown", f: OrderFulfillment{Order: newUnknownOrder(7)}, exp: false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual bool - testFunc := func() { - actual = tc.f.IsAskOrder() - } - require.NotPanics(t, testFunc, "IsAskOrder()") - assert.Equal(t, tc.exp, actual, "IsAskOrder() result") - }) - } -} - -func TestOrderFulfillment_IsBidOrder(t *testing.T) { - tests := []struct { - name string - f OrderFulfillment - exp bool - }{ - {name: "ask", f: OrderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: false}, - {name: "bid", f: OrderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: true}, - {name: "nil", f: OrderFulfillment{Order: NewOrder(888)}, exp: false}, - {name: "unknown", f: OrderFulfillment{Order: newUnknownOrder(9)}, exp: false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual bool - testFunc := func() { - actual = tc.f.IsBidOrder() - } - require.NotPanics(t, testFunc, "IsBidOrder()") - assert.Equal(t, tc.exp, actual, "IsBidOrder() result") - }) - } -} - -func TestOrderFulfillment_GetMarketID(t *testing.T) { - askOrder := func(marketID uint32) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithAsk(&AskOrder{MarketId: marketID}), - } - } - bidOrder := func(marketID uint32) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithBid(&BidOrder{MarketId: marketID}), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp uint32 - }{ - {name: "ask zero", f: askOrder(0), exp: 0}, - {name: "ask one", f: askOrder(1), exp: 1}, - {name: "ask five", f: askOrder(5), exp: 5}, - {name: "ask max uint16+1", f: askOrder(65_536), exp: 65_536}, - {name: "ask max uint32", f: askOrder(4_294_967_295), exp: 4_294_967_295}, - {name: "bid zero", f: bidOrder(0), exp: 0}, - {name: "bid one", f: bidOrder(1), exp: 1}, - {name: "bid five", f: bidOrder(5), exp: 5}, - {name: "bid max uint16+1", f: bidOrder(65_536), exp: 65_536}, - {name: "bid max uint32", f: bidOrder(4_294_967_295), exp: 4_294_967_295}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual uint32 - testFunc := func() { - actual = tc.f.GetMarketID() - } - require.NotPanics(t, testFunc, "GetMarketID()") - assert.Equal(t, tc.exp, actual, "GetMarketID() result") - }) - } -} - -func TestOrderFulfillment_GetOwner(t *testing.T) { - askOrder := func(seller string) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithAsk(&AskOrder{Seller: seller}), - } - } - bidOrder := func(buyer string) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithBid(&BidOrder{Buyer: buyer}), - } - } - owner := sdk.AccAddress("owner_______________").String() - - tests := []struct { - name string - f OrderFulfillment - exp string - }{ - {name: "ask empty", f: askOrder(""), exp: ""}, - {name: "ask not a bech32", f: askOrder("owner"), exp: "owner"}, - {name: "ask beche32", f: askOrder(owner), exp: owner}, - {name: "bid empty", f: bidOrder(""), exp: ""}, - {name: "bid not a bech32", f: bidOrder("owner"), exp: "owner"}, - {name: "bid beche32", f: bidOrder(owner), exp: owner}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual string - testFunc := func() { - actual = tc.f.GetOwner() - } - require.NotPanics(t, testFunc, "GetOwner()") - assert.Equal(t, tc.exp, actual, "GetOwner() result") - }) - } -} - -func TestOrderFulfillment_GetAssets(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(amt)} - } - askOrder := func(amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithAsk(&AskOrder{Assets: coin(amt)}), - } - } - bidOrder := func(amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithBid(&BidOrder{Assets: coin(amt)}), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "ask positive", f: askOrder(123), exp: coin(123)}, - {name: "ask zero", f: askOrder(0), exp: coin(0)}, - {name: "ask negative", f: askOrder(-9), exp: coin(-9)}, - {name: "bid positive", f: bidOrder(345), exp: coin(345)}, - {name: "bid zero", f: bidOrder(0), exp: coin(0)}, - {name: "bid negative", f: bidOrder(-8), exp: coin(-8)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetAssets() - } - require.NotPanics(t, testFunc, "GetAssets()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetAssets() result") - }) - } -} - -func TestOrderFulfillment_GetPrice(t *testing.T) { - coin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} - } - askOrder := func(amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithAsk(&AskOrder{Price: coin(amt)}), - } - } - bidOrder := func(amt int64) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithBid(&BidOrder{Price: coin(amt)}), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coin - }{ - {name: "ask positive", f: askOrder(123), exp: coin(123)}, - {name: "ask zero", f: askOrder(0), exp: coin(0)}, - {name: "ask negative", f: askOrder(-9), exp: coin(-9)}, - {name: "bid positive", f: bidOrder(345), exp: coin(345)}, - {name: "bid zero", f: bidOrder(0), exp: coin(0)}, - {name: "bid negative", f: bidOrder(-8), exp: coin(-8)}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coin - testFunc := func() { - actual = tc.f.GetPrice() - } - require.NotPanics(t, testFunc, "GetPrice()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetPrice() result") - }) - } -} - -func TestOrderFulfillment_GetSettlementFees(t *testing.T) { - coin := func(amt int64) *sdk.Coin { - return &sdk.Coin{Denom: "fees", Amount: sdkmath.NewInt(amt)} - } - askOrder := func(coin *sdk.Coin) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithAsk(&AskOrder{SellerSettlementFlatFee: coin}), - } - } - bidOrder := func(coins sdk.Coins) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithBid(&BidOrder{BuyerSettlementFees: coins}), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp sdk.Coins - }{ - {name: "ask nil", f: askOrder(nil), exp: nil}, - {name: "ask zero", f: askOrder(coin(0)), exp: sdk.Coins{*coin(0)}}, - {name: "ask positive", f: askOrder(coin(3)), exp: sdk.Coins{*coin(3)}}, - {name: "bid nil", f: bidOrder(nil), exp: nil}, - {name: "bid empty", f: bidOrder(sdk.Coins{}), exp: sdk.Coins{}}, - {name: "bid positive", f: bidOrder(sdk.Coins{*coin(3)}), exp: sdk.Coins{*coin(3)}}, - {name: "bid zero", f: bidOrder(sdk.Coins{*coin(0)}), exp: sdk.Coins{*coin(0)}}, - {name: "bid negative", f: bidOrder(sdk.Coins{*coin(-2)}), exp: sdk.Coins{*coin(-2)}}, - { - name: "bid multiple", - f: bidOrder(sdk.Coins{*coin(987), sdk.NewInt64Coin("six", 6), sdk.Coin{Denom: "zeg", Amount: sdkmath.NewInt(-1)}}), - exp: sdk.Coins{*coin(987), sdk.NewInt64Coin("six", 6), sdk.Coin{Denom: "zeg", Amount: sdkmath.NewInt(-1)}}, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdk.Coins - testFunc := func() { - actual = tc.f.GetSettlementFees() - } - require.NotPanics(t, testFunc, "GetSettlementFees()") - assert.Equal(t, tc.exp.String(), actual.String(), "GetSettlementFees() result") - }) - } -} - -func TestOrderFulfillment_PartialFillAllowed(t *testing.T) { - askOrder := func(allowPartial bool) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithAsk(&AskOrder{AllowPartial: allowPartial}), - } - } - bidOrder := func(allowPartial bool) OrderFulfillment { - return OrderFulfillment{ - Order: NewOrder(999).WithBid(&BidOrder{AllowPartial: allowPartial}), - } - } - - tests := []struct { - name string - f OrderFulfillment - exp bool - }{ - {name: "ask true", f: askOrder(true), exp: true}, - {name: "ask false", f: askOrder(false), exp: false}, - {name: "bid true", f: bidOrder(true), exp: true}, - {name: "bid false", f: bidOrder(false), exp: false}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual bool - testFunc := func() { - actual = tc.f.PartialFillAllowed() - } - require.NotPanics(t, testFunc, "PartialFillAllowed()") - assert.Equal(t, tc.exp, actual, "PartialFillAllowed() result") - }) - } -} - -func TestOrderFulfillment_GetOrderType(t *testing.T) { - tests := []struct { - name string - f OrderFulfillment - exp string - }{ - {name: "ask", f: OrderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: OrderTypeAsk}, - {name: "bid", f: OrderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: OrderTypeBid}, - {name: "nil", f: OrderFulfillment{Order: NewOrder(888)}, exp: ""}, - {name: "unknown", f: OrderFulfillment{Order: newUnknownOrder(8)}, exp: "*exchange.unknownOrderType"}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual string - testFunc := func() { - actual = tc.f.GetOrderType() - } - require.NotPanics(t, testFunc, "GetOrderType()") - assert.Equal(t, tc.exp, actual, "GetOrderType() result") - }) - } -} - -func TestOrderFulfillment_GetOrderTypeByte(t *testing.T) { - tests := []struct { - name string - f OrderFulfillment - exp byte - }{ - {name: "ask", f: OrderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: OrderTypeByteAsk}, - {name: "bid", f: OrderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: OrderTypeByteBid}, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual byte - testFunc := func() { - actual = tc.f.GetOrderTypeByte() - } - require.NotPanics(t, testFunc, "GetOrderTypeByte()") - assert.Equal(t, tc.exp, actual, "GetOrderTypeByte() result") - }) - } -} - -func TestOrderFulfillment_GetHoldAmount(t *testing.T) { - tests := []struct { - name string - f OrderFulfillment - }{ - { - name: "ask", - f: OrderFulfillment{ - Order: NewOrder(111).WithAsk(&AskOrder{ - Assets: sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(55)}, - SellerSettlementFlatFee: &sdk.Coin{Denom: "fee", Amount: sdkmath.NewInt(3)}, - }), - }, - }, - { - name: "bid", - f: OrderFulfillment{ - Order: NewOrder(111).WithBid(&BidOrder{ - Price: sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(55)}, - BuyerSettlementFees: sdk.Coins{ - {Denom: "feea", Amount: sdkmath.NewInt(3)}, - {Denom: "feeb", Amount: sdkmath.NewInt(4)}, - }, - }), - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - expected := tc.f.GetHoldAmount() - var actual sdk.Coins - testFunc := func() { - actual = tc.f.GetHoldAmount() - } - require.NotPanics(t, testFunc, "GetHoldAmount()") - assert.Equal(t, expected, actual, "GetHoldAmount() result") - }) - } -} - -func TestOrderFulfillment_DistributeAssets(t *testing.T) { - newOF := func(order *Order, assetsUnfilled, assetsFilled int64, dists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), - AssetsFilledAmt: sdkmath.NewInt(assetsFilled), - } - if assetsUnfilled == 0 { - rv.AssetsUnfilledAmt = ZeroAmtAfterSub - } - if len(dists) > 0 { - rv.AssetDists = dists - } - return rv - - } - askOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithAsk(&AskOrder{Assets: sdk.NewInt64Coin("apple", 999)}) - return newOF(order, assetsUnfilled, assetsFilled, dists...) - } - bidOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithBid(&BidOrder{Assets: sdk.NewInt64Coin("apple", 999)}) - return newOF(order, assetsUnfilled, assetsFilled, dists...) - } - dist := func(addr string, amt int64) *Distribution { - return &Distribution{ - Address: addr, - Amount: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - receiver *OrderFulfillment - order OrderI - amount sdkmath.Int - expRes *OrderFulfillment - expErr string - }{ - { - name: "assets unfilled less than amount: ask, ask", - receiver: askOF(1, 5, 0), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(6), - expErr: "cannot fill ask order 1 having assets left \"5apple\" with \"6apple\" from ask order 2: overfill", - }, - { - name: "assets unfilled less than amount: ask, bid", - receiver: askOF(3, 5, 0), - order: NewOrder(4).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(6), - expErr: "cannot fill ask order 3 having assets left \"5apple\" with \"6apple\" from bid order 4: overfill", - }, - { - name: "assets unfilled less than amount: bid, ask", - receiver: bidOF(5, 5, 0), - order: NewOrder(6).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(6), - expErr: "cannot fill bid order 5 having assets left \"5apple\" with \"6apple\" from ask order 6: overfill", - }, - { - name: "assets unfilled less than amount: bid, bid", - receiver: bidOF(7, 5, 0), - order: NewOrder(8).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(6), - expErr: "cannot fill bid order 7 having assets left \"5apple\" with \"6apple\" from bid order 8: overfill", - }, - { - name: "assets unfilled equals amount: ask, bid", - receiver: askOF(1, 12345, 0), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(12345), - expRes: askOF(1, 0, 12345, dist("buYer", 12345)), - }, - { - name: "assets unfilled equals amount: bid, ask", - receiver: bidOF(1, 12345, 0), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(12345), - expRes: bidOF(1, 0, 12345, dist("seLLer", 12345)), - }, - { - name: "assets unfilled more than amount: ask, bid", - receiver: askOF(1, 12345, 0), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(300), - expRes: askOF(1, 12045, 300, dist("buYer", 300)), - }, - { - name: "assets unfilled more than amount: bid, ask", - receiver: bidOF(1, 12345, 0), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(300), - expRes: bidOF(1, 12045, 300, dist("seLLer", 300)), - }, - { - name: "already has 2 dists: ask, bid", - receiver: askOF(1, 12300, 45, dist("bbbbb", 40), dist("YYYYY", 5)), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(2000), - expRes: askOF(1, 10300, 2045, dist("bbbbb", 40), dist("YYYYY", 5), dist("buYer", 2000)), - }, - { - name: "already has 2 dists: bid, ask", - receiver: bidOF(1, 12300, 45, dist("sssss", 40), dist("LLLLL", 5)), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(2000), - expRes: bidOF(1, 10300, 2045, dist("sssss", 40), dist("LLLLL", 5), dist("seLLer", 2000)), - }, - { - name: "amt more than filled, ask, bid", - receiver: askOF(1, 45, 12300, dist("ssss", 12300)), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(45), - expRes: askOF(1, 0, 12345, dist("ssss", 12300), dist("buYer", 45)), - }, - { - name: "amt more than filled, bid, ask", - receiver: bidOF(1, 45, 12300, dist("ssss", 12300)), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(45), - expRes: bidOF(1, 0, 12345, dist("ssss", 12300), dist("seLLer", 45)), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - orig := copyOrderFulfillment(tc.receiver) - if tc.expRes == nil { - tc.expRes = copyOrderFulfillment(tc.receiver) - } - var err error - testFunc := func() { - err = tc.receiver.DistributeAssets(tc.order, tc.amount) - } - require.NotPanics(t, testFunc, "DistributeAssets") - assertions.AssertErrorValue(t, err, tc.expErr, "DistributeAssets error") - if !assertEqualOrderFulfillments(t, tc.expRes, tc.receiver, "OrderFulfillment after DistributeAssets") { - t.Logf("Original: %s", orderFulfillmentString(orig)) - t.Logf(" Amount: %s", tc.amount) - } - }) - } -} - -func TestDistributeAssets(t *testing.T) { - seller, buyer := "SelleR", "BuyeR" - newOF := func(order *Order, assetsUnfilled, assetsFilled int64, dists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), - AssetsFilledAmt: sdkmath.NewInt(assetsFilled), - } - if assetsUnfilled == 0 { - rv.AssetsUnfilledAmt = ZeroAmtAfterSub - } - if len(dists) > 0 { - rv.AssetDists = dists - } - return rv - - } - askOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithAsk(&AskOrder{ - Seller: seller, - Assets: sdk.NewInt64Coin("apple", 999), - }) - return newOF(order, assetsUnfilled, assetsFilled, dists...) - } - bidOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithBid(&BidOrder{ - Buyer: buyer, - Assets: sdk.NewInt64Coin("apple", 999), - }) - return newOF(order, assetsUnfilled, assetsFilled, dists...) - } - dist := func(addr string, amt int64) *Distribution { - return &Distribution{ - Address: addr, - Amount: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - of1 *OrderFulfillment - of2 *OrderFulfillment - amount sdkmath.Int - expOF1 *OrderFulfillment - expOF2 *OrderFulfillment - expErr string - }{ - { - name: "amount more than of1 unfilled: ask bid", - of1: askOF(1, 5, 0), - of2: bidOF(2, 6, 0), - amount: sdkmath.NewInt(6), - expOF2: bidOF(2, 0, 6, dist(seller, 6)), - expErr: "cannot fill ask order 1 having assets left \"5apple\" with \"6apple\" from bid order 2: overfill", - }, - { - name: "amount more than of1 unfilled: bid ask", - of1: bidOF(1, 5, 0), - of2: askOF(2, 6, 0), - amount: sdkmath.NewInt(6), - expOF2: askOF(2, 0, 6, dist(buyer, 6)), - expErr: "cannot fill bid order 1 having assets left \"5apple\" with \"6apple\" from ask order 2: overfill", - }, - { - name: "amount more than of2 unfilled: ask, bid", - of1: askOF(1, 6, 0), - of2: bidOF(2, 5, 0), - amount: sdkmath.NewInt(6), - expOF1: askOF(1, 0, 6, dist(buyer, 6)), - expErr: "cannot fill bid order 2 having assets left \"5apple\" with \"6apple\" from ask order 1: overfill", - }, - { - name: "amount more than of2 unfilled: bid, ask", - of1: bidOF(1, 6, 0), - of2: askOF(2, 5, 0), - amount: sdkmath.NewInt(6), - expOF1: bidOF(1, 0, 6, dist(seller, 6)), - expErr: "cannot fill ask order 2 having assets left \"5apple\" with \"6apple\" from bid order 1: overfill", - }, - { - name: "amount more than both unfilled: ask, bid", - of1: askOF(1, 5, 0), - of2: bidOF(2, 4, 0), - amount: sdkmath.NewInt(6), - expErr: "cannot fill ask order 1 having assets left \"5apple\" with \"6apple\" from bid order 2: overfill" + "\n" + - "cannot fill bid order 2 having assets left \"4apple\" with \"6apple\" from ask order 1: overfill", - }, - { - name: "amount more than both unfilled: ask, bid", - of1: bidOF(1, 5, 0), - of2: askOF(2, 4, 0), - amount: sdkmath.NewInt(6), - expErr: "cannot fill bid order 1 having assets left \"5apple\" with \"6apple\" from ask order 2: overfill" + "\n" + - "cannot fill ask order 2 having assets left \"4apple\" with \"6apple\" from bid order 1: overfill", - }, - { - name: "ask bid", - of1: askOF(1, 10, 55, dist("bbb", 55)), - of2: bidOF(2, 10, 0), - amount: sdkmath.NewInt(9), - expOF1: askOF(1, 1, 64, dist("bbb", 55), dist(buyer, 9)), - expOF2: bidOF(2, 1, 9, dist(seller, 9)), - }, - { - name: "bid ask", - of1: bidOF(1, 10, 55, dist("sss", 55)), - of2: askOF(2, 10, 3, dist("bbb", 3)), - amount: sdkmath.NewInt(10), - expOF1: bidOF(1, 0, 65, dist("sss", 55), dist(seller, 10)), - expOF2: askOF(2, 0, 13, dist("bbb", 3), dist(buyer, 10)), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - origOF1 := copyOrderFulfillment(tc.of1) - origOF2 := copyOrderFulfillment(tc.of2) - if tc.expOF1 == nil { - tc.expOF1 = copyOrderFulfillment(tc.of1) - } - if tc.expOF2 == nil { - tc.expOF2 = copyOrderFulfillment(tc.of2) - } - - var err error - testFunc := func() { - err = DistributeAssets(tc.of1, tc.of2, tc.amount) - } - require.NotPanics(t, testFunc, "DistributeAssets") - assertions.AssertErrorValue(t, err, tc.expErr, "DistributeAssets error") - if !assertEqualOrderFulfillments(t, tc.expOF1, tc.of1, "of1 after DistributeAssets") { - t.Logf("Original: %s", orderFulfillmentString(origOF1)) - t.Logf(" Amount: %s", tc.amount) - } - if !assertEqualOrderFulfillments(t, tc.expOF2, tc.of2, "of2 after DistributeAssets") { - t.Logf("Original: %s", orderFulfillmentString(origOF2)) - t.Logf(" Amount: %s", tc.amount) - } - }) - } -} - -func TestOrderFulfillment_DistributePrice(t *testing.T) { - newOF := func(order *Order, priceLeft, priceApplied int64, dists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - PriceLeftAmt: sdkmath.NewInt(priceLeft), - PriceAppliedAmt: sdkmath.NewInt(priceApplied), - } - if priceLeft == 0 { - rv.PriceLeftAmt = ZeroAmtAfterSub - } - if len(dists) > 0 { - rv.PriceDists = dists - } - return rv - - } - askOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithAsk(&AskOrder{Price: sdk.NewInt64Coin("peach", 999)}) - return newOF(order, priceLeft, priceApplied, dists...) - } - bidOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithBid(&BidOrder{Price: sdk.NewInt64Coin("peach", 999)}) - return newOF(order, priceLeft, priceApplied, dists...) - } - dist := func(addr string, amt int64) *Distribution { - return &Distribution{ - Address: addr, - Amount: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - receiver *OrderFulfillment - order OrderI - amount sdkmath.Int - expRes *OrderFulfillment - expErr string - }{ - { - name: "assets unfilled less than amount: ask, ask", - receiver: askOF(1, 5, 0), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(6), - expRes: askOF(1, -1, 6, dist("seLLer", 6)), - }, - { - name: "assets unfilled less than amount: ask, bid", - receiver: askOF(3, 5, 0), - order: NewOrder(4).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(6), - expRes: askOF(3, -1, 6, dist("buYer", 6)), - }, - { - name: "assets unfilled less than amount: bid, ask", - receiver: bidOF(5, 5, 0), - order: NewOrder(6).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(6), - expErr: "cannot fill bid order 5 having price left \"5peach\" to ask order 6 at a price of \"6peach\": overfill", - }, - { - name: "assets unfilled less than amount: bid, bid", - receiver: bidOF(7, 5, 0), - order: NewOrder(8).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(6), - expErr: "cannot fill bid order 7 having price left \"5peach\" to bid order 8 at a price of \"6peach\": overfill", - }, - { - name: "assets unfilled equals amount: ask, bid", - receiver: askOF(1, 12345, 0), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(12345), - expRes: askOF(1, 0, 12345, dist("buYer", 12345)), - }, - { - name: "assets unfilled equals amount: bid, ask", - receiver: bidOF(1, 12345, 0), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(12345), - expRes: bidOF(1, 0, 12345, dist("seLLer", 12345)), - }, - { - name: "assets unfilled more than amount: ask, bid", - receiver: askOF(1, 12345, 0), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(300), - expRes: askOF(1, 12045, 300, dist("buYer", 300)), - }, - { - name: "assets unfilled more than amount: bid, ask", - receiver: bidOF(1, 12345, 0), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(300), - expRes: bidOF(1, 12045, 300, dist("seLLer", 300)), - }, - { - name: "already has 2 dists: ask, bid", - receiver: askOF(1, 12300, 45, dist("bbbbb", 40), dist("YYYYY", 5)), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(2000), - expRes: askOF(1, 10300, 2045, dist("bbbbb", 40), dist("YYYYY", 5), dist("buYer", 2000)), - }, - { - name: "already has 2 dists: bid, ask", - receiver: bidOF(1, 12300, 45, dist("sssss", 40), dist("LLLLL", 5)), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(2000), - expRes: bidOF(1, 10300, 2045, dist("sssss", 40), dist("LLLLL", 5), dist("seLLer", 2000)), - }, - { - name: "amt more than filled, ask, bid", - receiver: askOF(1, 45, 12300, dist("ssss", 12300)), - order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), - amount: sdkmath.NewInt(45), - expRes: askOF(1, 0, 12345, dist("ssss", 12300), dist("buYer", 45)), - }, - { - name: "amt more than filled, bid, ask", - receiver: bidOF(1, 45, 12300, dist("ssss", 12300)), - order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), - amount: sdkmath.NewInt(45), - expRes: bidOF(1, 0, 12345, dist("ssss", 12300), dist("seLLer", 45)), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - orig := copyOrderFulfillment(tc.receiver) - if tc.expRes == nil { - tc.expRes = copyOrderFulfillment(tc.receiver) - } - var err error - testFunc := func() { - err = tc.receiver.DistributePrice(tc.order, tc.amount) - } - require.NotPanics(t, testFunc, "DistributePrice") - assertions.AssertErrorValue(t, err, tc.expErr, "DistributePrice error") - if !assertEqualOrderFulfillments(t, tc.expRes, tc.receiver, "OrderFulfillment after DistributePrice") { - t.Logf("Original: %s", orderFulfillmentString(orig)) - t.Logf(" Amount: %s", tc.amount) - } - }) - } -} - -func TestDistributePrice(t *testing.T) { - seller, buyer := "SelleR", "BuyeR" - newOF := func(order *Order, priceLeft, priceApplied int64, dists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - PriceLeftAmt: sdkmath.NewInt(priceLeft), - PriceAppliedAmt: sdkmath.NewInt(priceApplied), - } - if priceLeft == 0 { - rv.PriceLeftAmt = ZeroAmtAfterSub - } - if len(dists) > 0 { - rv.PriceDists = dists - } - return rv - - } - askOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithAsk(&AskOrder{ - Seller: seller, - Price: sdk.NewInt64Coin("peach", 999), - }) - return newOF(order, priceLeft, priceApplied, dists...) - } - bidOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*Distribution) *OrderFulfillment { - order := NewOrder(orderID).WithBid(&BidOrder{ - Buyer: buyer, - Price: sdk.NewInt64Coin("peach", 999), - }) - return newOF(order, priceLeft, priceApplied, dists...) - } - dist := func(addr string, amt int64) *Distribution { - return &Distribution{ - Address: addr, - Amount: sdkmath.NewInt(amt), - } - } - - tests := []struct { - name string - of1 *OrderFulfillment - of2 *OrderFulfillment - amount sdkmath.Int - expOF1 *OrderFulfillment - expOF2 *OrderFulfillment - expErr string - }{ - { - name: "amount more than of1 unfilled: ask bid", - of1: askOF(1, 5, 0), - of2: bidOF(2, 6, 0), - amount: sdkmath.NewInt(6), - expOF1: askOF(1, -1, 6, dist(buyer, 6)), - expOF2: bidOF(2, 0, 6, dist(seller, 6)), - }, - { - name: "amount more than of1 unfilled: bid ask", - of1: bidOF(1, 5, 0), - of2: askOF(2, 6, 0), - amount: sdkmath.NewInt(6), - expOF2: askOF(2, 0, 6, dist(buyer, 6)), - expErr: "cannot fill bid order 1 having price left \"5peach\" to ask order 2 at a price of \"6peach\": overfill", - }, - { - name: "amount more than of2 unfilled: ask, bid", - of1: askOF(1, 6, 0), - of2: bidOF(2, 5, 0), - amount: sdkmath.NewInt(6), - expOF1: askOF(1, 0, 6, dist(buyer, 6)), - expErr: "cannot fill bid order 2 having price left \"5peach\" to ask order 1 at a price of \"6peach\": overfill", - }, - { - name: "amount more than of2 unfilled: bid, ask", - of1: bidOF(1, 6, 0), - of2: askOF(2, 5, 0), - amount: sdkmath.NewInt(6), - expOF1: bidOF(1, 0, 6, dist(seller, 6)), - expOF2: askOF(2, -1, 6, dist(buyer, 6)), - }, - { - name: "amount more than both unfilled: ask, bid", - of1: askOF(1, 5, 0), - of2: bidOF(2, 4, 0), - amount: sdkmath.NewInt(6), - expOF1: askOF(1, -1, 6, dist(buyer, 6)), - expErr: "cannot fill bid order 2 having price left \"4peach\" to ask order 1 at a price of \"6peach\": overfill", - }, - { - name: "amount more than both unfilled: ask, bid", - of1: bidOF(1, 5, 0), - of2: askOF(2, 4, 0), - amount: sdkmath.NewInt(6), - expOF2: askOF(2, -2, 6, dist(buyer, 6)), - expErr: "cannot fill bid order 1 having price left \"5peach\" to ask order 2 at a price of \"6peach\": overfill", - }, - { - name: "ask bid", - of1: askOF(1, 10, 55, dist("bbb", 55)), - of2: bidOF(2, 10, 0), - amount: sdkmath.NewInt(9), - expOF1: askOF(1, 1, 64, dist("bbb", 55), dist(buyer, 9)), - expOF2: bidOF(2, 1, 9, dist(seller, 9)), - }, - { - name: "bid ask", - of1: bidOF(1, 10, 55, dist("sss", 55)), - of2: askOF(2, 10, 3, dist("bbb", 3)), - amount: sdkmath.NewInt(10), - expOF1: bidOF(1, 0, 65, dist("sss", 55), dist(seller, 10)), - expOF2: askOF(2, 0, 13, dist("bbb", 3), dist(buyer, 10)), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - origOF1 := copyOrderFulfillment(tc.of1) - origOF2 := copyOrderFulfillment(tc.of2) - if tc.expOF1 == nil { - tc.expOF1 = copyOrderFulfillment(tc.of1) - } - if tc.expOF2 == nil { - tc.expOF2 = copyOrderFulfillment(tc.of2) - } - - var err error - testFunc := func() { - err = DistributePrice(tc.of1, tc.of2, tc.amount) - } - require.NotPanics(t, testFunc, "DistributePrice") - assertions.AssertErrorValue(t, err, tc.expErr, "DistributePrice error") - if !assertEqualOrderFulfillments(t, tc.expOF1, tc.of1, "of1 after DistributePrice") { - t.Logf("Original: %s", orderFulfillmentString(origOF1)) - t.Logf(" Amount: %s", tc.amount) - } - if !assertEqualOrderFulfillments(t, tc.expOF2, tc.of2, "of2 after DistributePrice") { - t.Logf("Original: %s", orderFulfillmentString(origOF2)) - t.Logf(" Amount: %s", tc.amount) - } - }) - } -} - -func TestOrderFulfillment_SplitOrder(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - askOrder := func(orderID uint64, assetAmt, priceAmt int64, fees ...sdk.Coin) *Order { - askOrder := &AskOrder{ - MarketId: 55, - Seller: "samuel", - Assets: coin(assetAmt, "apple"), - Price: coin(priceAmt, "peach"), - AllowPartial: true, - } - if len(fees) > 1 { - t.Fatalf("a max of 1 fee can be provided to askOrder, actual: %s", sdk.Coins(fees)) - } - if len(fees) > 0 { - askOrder.SellerSettlementFlatFee = &fees[0] - } - return NewOrder(orderID).WithAsk(askOrder) - } - bidOrder := func(orderID uint64, assetAmt, priceAmt int64, fees ...sdk.Coin) *Order { - bidOrder := &BidOrder{ - MarketId: 55, - Buyer: "brian", - Assets: coin(assetAmt, "apple"), - Price: coin(priceAmt, "peach"), - AllowPartial: true, - } - if len(fees) > 0 { - bidOrder.BuyerSettlementFees = fees - } - return NewOrder(orderID).WithBid(bidOrder) - } - - tests := []struct { - name string - receiver *OrderFulfillment - expUnfilled *Order - expReceiver *OrderFulfillment - expErr string - }{ - { - name: "order split error: ask", - receiver: &OrderFulfillment{ - Order: askOrder(8, 10, 100), - AssetsFilledAmt: sdkmath.NewInt(-1), - }, - expErr: "cannot split ask order 8 having asset \"10apple\" at \"-1apple\": amount filled not positive", - }, - { - name: "order split error: bid", - receiver: &OrderFulfillment{ - Order: bidOrder(9, 10, 100), - AssetsFilledAmt: sdkmath.NewInt(-1), - }, - expErr: "cannot split bid order 9 having asset \"10apple\" at \"-1apple\": amount filled not positive", - }, - { - name: "okay: ask", - receiver: &OrderFulfillment{ - Order: askOrder(17, 10, 100, coin(20, "fig")), - AssetsFilledAmt: sdkmath.NewInt(9), - AssetsUnfilledAmt: sdkmath.NewInt(1), - PriceAppliedAmt: sdkmath.NewInt(300), - PriceLeftAmt: sdkmath.NewInt(-200), - }, - expUnfilled: askOrder(17, 1, 10, coin(2, "fig")), - expReceiver: &OrderFulfillment{ - Order: askOrder(17, 9, 90, coin(18, "fig")), - AssetsFilledAmt: sdkmath.NewInt(9), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(300), - PriceLeftAmt: sdkmath.NewInt(-210), - }, - }, - { - name: "okay: bid", - receiver: &OrderFulfillment{ - Order: bidOrder(19, 10, 100, coin(20, "fig")), - AssetsFilledAmt: sdkmath.NewInt(9), - AssetsUnfilledAmt: sdkmath.NewInt(1), - PriceAppliedAmt: sdkmath.NewInt(300), - PriceLeftAmt: sdkmath.NewInt(-200), - }, - expUnfilled: bidOrder(19, 1, 10, coin(2, "fig")), - expReceiver: &OrderFulfillment{ - Order: bidOrder(19, 9, 90, coin(18, "fig")), - AssetsFilledAmt: sdkmath.NewInt(9), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(300), - PriceLeftAmt: sdkmath.NewInt(-210), - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - orig := copyOrderFulfillment(tc.receiver) - if tc.expReceiver == nil { - tc.expReceiver = copyOrderFulfillment(tc.receiver) - } - - var unfilled *Order - var err error - testFunc := func() { - unfilled, err = tc.receiver.SplitOrder() - } - require.NotPanics(t, testFunc, "SplitOrder") - assertions.AssertErrorValue(t, err, tc.expErr, "SplitOrder error") - assert.Equalf(t, tc.expUnfilled, unfilled, "SplitOrder unfilled order") - if !assertEqualOrderFulfillments(t, tc.expReceiver, tc.receiver, "OrderFulfillment after SplitOrder") { - t.Logf("Original: %s", orderFulfillmentString(orig)) - } - }) - } -} - -func TestOrderFulfillment_AsFilledOrder(t *testing.T) { - askOrder := NewOrder(53).WithAsk(&AskOrder{ - MarketId: 765, - Seller: "mefirst", - Assets: sdk.NewInt64Coin("apple", 15), - Price: sdk.NewInt64Coin("peach", 88), - SellerSettlementFlatFee: &sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(6)}, - AllowPartial: true, - }) - bidOrder := NewOrder(9556).WithBid(&BidOrder{ - MarketId: 145, - Buyer: "gimmiegimmie", - Assets: sdk.NewInt64Coin("acorn", 1171), - Price: sdk.NewInt64Coin("prune", 5100), - BuyerSettlementFees: sdk.NewCoins(sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(14)}), - AllowPartial: false, - }) - - tests := []struct { - name string - receiver OrderFulfillment - expected *FilledOrder - }{ - { - name: "ask order", - receiver: OrderFulfillment{ - Order: askOrder, - PriceAppliedAmt: sdkmath.NewInt(132), - FeesToPay: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(7)}), - }, - expected: &FilledOrder{ - order: askOrder, - actualPrice: sdk.NewInt64Coin("peach", 132), - actualFees: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(7)}), - }, - }, - { - name: "bid order", - receiver: OrderFulfillment{ - Order: bidOrder, - PriceAppliedAmt: sdkmath.NewInt(5123), - FeesToPay: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(23)}), - }, - expected: &FilledOrder{ - order: bidOrder, - actualPrice: sdk.NewInt64Coin("prune", 5123), - actualFees: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(23)}), - }, - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual *FilledOrder - testFunc := func() { - actual = tc.receiver.AsFilledOrder() - } - require.NotPanics(t, testFunc, "AsFilledOrder()") - assert.Equal(t, tc.expected, actual, "AsFilledOrder() result") - }) - } -} - -func TestSumAssetsAndPrice(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - Assets: assets, - Price: price, - }) - } - bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Assets: assets, - Price: price, - }) - } - - tests := []struct { - name string - orders []*Order - expAssets sdk.Coins - expPrice sdk.Coins - expPanic string - }{ - { - name: "nil orders", - orders: nil, - expAssets: nil, - expPrice: nil, - }, - { - name: "empty orders", - orders: []*Order{}, - expAssets: nil, - expPrice: nil, - }, - { - name: "nil inside order", - orders: []*Order{ - askOrder(1, coin(2, "apple"), coin(3, "plum")), - NewOrder(4), - askOrder(5, coin(6, "apple"), coin(7, "plum")), - }, - expPanic: nilSubTypeErr(4), - }, - { - name: "unknown inside order", - orders: []*Order{ - askOrder(1, coin(2, "apple"), coin(3, "plum")), - newUnknownOrder(4), - askOrder(5, coin(6, "apple"), coin(7, "plum")), - }, - expPanic: unknownSubTypeErr(4), - }, - { - name: "one order, ask", - orders: []*Order{askOrder(1, coin(2, "apple"), coin(3, "plum"))}, - expAssets: sdk.NewCoins(coin(2, "apple")), - expPrice: sdk.NewCoins(coin(3, "plum")), - }, - { - name: "one order, bid", - orders: []*Order{bidOrder(1, coin(2, "apple"), coin(3, "plum"))}, - expAssets: sdk.NewCoins(coin(2, "apple")), - expPrice: sdk.NewCoins(coin(3, "plum")), - }, - { - name: "2 orders, same denoms", - orders: []*Order{ - askOrder(1, coin(2, "apple"), coin(3, "plum")), - bidOrder(4, coin(5, "apple"), coin(6, "plum")), - }, - expAssets: sdk.NewCoins(coin(7, "apple")), - expPrice: sdk.NewCoins(coin(9, "plum")), - }, - { - name: "2 orders, diff denoms", - orders: []*Order{ - bidOrder(1, coin(2, "avocado"), coin(3, "peach")), - askOrder(4, coin(5, "apple"), coin(6, "plum")), - }, - expAssets: sdk.NewCoins(coin(2, "avocado"), coin(5, "apple")), - expPrice: sdk.NewCoins(coin(3, "peach"), coin(6, "plum")), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var assets, price sdk.Coins - testFunc := func() { - assets, price = sumAssetsAndPrice(tc.orders) - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "sumAssetsAndPrice") - assert.Equal(t, tc.expAssets.String(), assets.String(), "sumAssetsAndPrice") - assert.Equal(t, tc.expPrice.String(), price.String(), "sumAssetsAndPrice") - }) - } -} - -func TestSumPriceLeft(t *testing.T) { - tests := []struct { - name string - fulfillments []*OrderFulfillment - expected sdkmath.Int - }{ - { - name: "nil fulfillments", - fulfillments: nil, - expected: sdkmath.NewInt(0), - }, - { - name: "empty fulfillments", - fulfillments: []*OrderFulfillment{}, - expected: sdkmath.NewInt(0), - }, - { - name: "one fulfillment, positive", - fulfillments: []*OrderFulfillment{{PriceLeftAmt: sdkmath.NewInt(8)}}, - expected: sdkmath.NewInt(8), - }, - { - name: "one fulfillment, zero", - fulfillments: []*OrderFulfillment{{PriceLeftAmt: sdkmath.NewInt(0)}}, - expected: sdkmath.NewInt(0), - }, - { - name: "one fulfillment, negative", - fulfillments: []*OrderFulfillment{{PriceLeftAmt: sdkmath.NewInt(-3)}}, - expected: sdkmath.NewInt(-3), - }, - { - name: "three fulfillments", - fulfillments: []*OrderFulfillment{ - {PriceLeftAmt: sdkmath.NewInt(10)}, - {PriceLeftAmt: sdkmath.NewInt(200)}, - {PriceLeftAmt: sdkmath.NewInt(3000)}, - }, - expected: sdkmath.NewInt(3210), - }, - { - name: "three fulfillments, one negative", - fulfillments: []*OrderFulfillment{ - {PriceLeftAmt: sdkmath.NewInt(10)}, - {PriceLeftAmt: sdkmath.NewInt(-200)}, - {PriceLeftAmt: sdkmath.NewInt(3000)}, - }, - expected: sdkmath.NewInt(2810), - }, - { - name: "three fulfillments, all negative", - fulfillments: []*OrderFulfillment{ - {PriceLeftAmt: sdkmath.NewInt(-10)}, - {PriceLeftAmt: sdkmath.NewInt(-200)}, - {PriceLeftAmt: sdkmath.NewInt(-3000)}, - }, - expected: sdkmath.NewInt(-3210), - }, - { - name: "three fulfillments, all large", - fulfillments: []*OrderFulfillment{ - {PriceLeftAmt: newInt(t, "3,000,000,000,000,000,000,000,000,000,000,300")}, - {PriceLeftAmt: newInt(t, "40,000,000,000,000,000,000,000,000,000,000,040")}, - {PriceLeftAmt: newInt(t, "500,000,000,000,000,000,000,000,000,000,000,005")}, - }, - expected: newInt(t, "543,000,000,000,000,000,000,000,000,000,000,345"), - }, - { - name: "four fullfillments, small negative zero large", - fulfillments: []*OrderFulfillment{ - {PriceLeftAmt: sdkmath.NewInt(654_789)}, - {PriceLeftAmt: sdkmath.NewInt(-789)}, - {PriceLeftAmt: sdkmath.NewInt(0)}, - {PriceLeftAmt: newInt(t, "543,000,000,000,000,000,000,000,000,000,000,345")}, - }, - expected: newInt(t, "543,000,000,000,000,000,000,000,000,000,654,345"), - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var actual sdkmath.Int - testFunc := func() { - actual = sumPriceLeft(tc.fulfillments) - } - require.NotPanics(t, testFunc, "sumPriceLeft") - assert.Equal(t, tc.expected, actual, "sumPriceLeft") - }) - } -} - -func TestBuildSettlement(t *testing.T) { - assetDenom, priceDenom := "apple", "peach" - feeDenoms := []string{"fig", "grape"} - feeCoins := func(tracer string, amts []int64) sdk.Coins { - if len(amts) == 0 { - return nil - } - if len(amts) > len(feeDenoms) { - t.Fatalf("cannot create %s with more than %d fees %v", tracer, len(feeDenoms), amts) - } - var rv sdk.Coins - for i, amt := range amts { - rv = rv.Add(sdk.NewInt64Coin(feeDenoms[i], amt)) - } - return rv - } - askOrder := func(orderID uint64, assets, price int64, allowPartial bool, fees ...int64) *Order { - if len(fees) > 1 { - t.Fatalf("cannot create ask order %d with more than 1 fees %v", orderID, fees) - } - var fee *sdk.Coin - if fc := feeCoins("", fees); !fc.IsZero() { - fee = &fc[0] - } - return NewOrder(orderID).WithAsk(&AskOrder{ - Seller: fmt.Sprintf("seller%d", orderID), - Assets: sdk.NewInt64Coin(assetDenom, assets), - Price: sdk.NewInt64Coin(priceDenom, price), - SellerSettlementFlatFee: fee, - AllowPartial: allowPartial, - }) - } - bidOrder := func(orderID uint64, assets, price int64, allowPartial bool, fees ...int64) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Buyer: fmt.Sprintf("buyer%d", orderID), - Assets: sdk.NewInt64Coin(assetDenom, assets), - Price: sdk.NewInt64Coin(priceDenom, price), - BuyerSettlementFees: feeCoins(fmt.Sprintf("bid order %d", orderID), fees), - AllowPartial: allowPartial, - }) - } - ratio := func(price, fee int64) func(denom string) (*FeeRatio, error) { - return func(denom string) (*FeeRatio, error) { - return &FeeRatio{Price: sdk.NewInt64Coin(priceDenom, price), Fee: sdk.NewInt64Coin(feeDenoms[0], fee)}, nil - } - } - filled := func(order *Order, price int64, fees ...int64) *FilledOrder { - return NewFilledOrder(order, - sdk.NewInt64Coin(priceDenom, price), - feeCoins(fmt.Sprintf("filled order %d", order), fees)) - } - assetsInput := func(orderID uint64, amount int64) banktypes.Input { - return banktypes.Input{ - Address: fmt.Sprintf("seller%d", orderID), - Coins: sdk.NewCoins(sdk.NewInt64Coin(assetDenom, amount)), - } - } - assetsOutput := func(orderID uint64, amount int64) banktypes.Output { - return banktypes.Output{ - Address: fmt.Sprintf("buyer%d", orderID), - Coins: sdk.NewCoins(sdk.NewInt64Coin(assetDenom, amount)), - } - } - priceInput := func(orderID uint64, amount int64) banktypes.Input { - return banktypes.Input{ - Address: fmt.Sprintf("buyer%d", orderID), - Coins: sdk.NewCoins(sdk.NewInt64Coin(priceDenom, amount)), - } - } - priceOutput := func(orderID uint64, amount int64) banktypes.Output { - return banktypes.Output{ - Address: fmt.Sprintf("seller%d", orderID), - Coins: sdk.NewCoins(sdk.NewInt64Coin(priceDenom, amount)), - } - } - feeInput := func(address string, amts ...int64) banktypes.Input { - return banktypes.Input{Address: address, Coins: feeCoins("bank input for "+address, amts)} - } - - tests := []struct { - name string - askOrders []*Order - bidOrders []*Order - sellerFeeRatioLookup func(denom string) (*FeeRatio, error) - expSettlement *Settlement - expErr string - }{ - { - // error from validateCanSettle - name: "no ask orders", - askOrders: []*Order{}, - bidOrders: []*Order{bidOrder(3, 1, 10, false)}, - expErr: "no ask orders provided", - }, - { - name: "error from ratio lookup", - askOrders: []*Order{askOrder(3, 1, 10, false)}, - bidOrders: []*Order{bidOrder(4, 1, 10, false)}, - sellerFeeRatioLookup: func(denom string) (*FeeRatio, error) { - return nil, errors.New("this is a test error") - }, - expErr: "this is a test error", - }, - { - name: "error from setFeesToPay", - askOrders: []*Order{askOrder(3, 1, 10, false)}, - bidOrders: []*Order{bidOrder(4, 1, 10, false)}, - sellerFeeRatioLookup: func(denom string) (*FeeRatio, error) { - return &FeeRatio{Price: sdk.NewInt64Coin("prune", 10), Fee: sdk.NewInt64Coin("fig", 1)}, nil - }, - expErr: "failed calculate ratio fee for ask order 3: cannot apply ratio 10prune:1fig to price 10peach: incorrect price denom", - }, - { - name: "one ask, three bids: last bid not used", - askOrders: []*Order{askOrder(3, 10, 20, false)}, - bidOrders: []*Order{ - bidOrder(4, 7, 14, false), - bidOrder(5, 3, 6, false), - bidOrder(6, 1, 2, false), - }, - expErr: "bid order 6 (at index 2) has no assets filled", - }, - { - name: "three asks, one bids: last ask not used", - askOrders: []*Order{ - askOrder(11, 7, 14, false), - askOrder(12, 3, 14, false), - askOrder(13, 1, 14, false), - }, - bidOrders: []*Order{bidOrder(14, 10, 20, false)}, - expErr: "ask order 13 (at index 2) has no assets filled", - }, - { - name: "two asks, two bids: same assets total, total bid price not enough", - askOrders: []*Order{ - askOrder(1, 10, 25, false), - askOrder(2, 10, 15, false), - }, - bidOrders: []*Order{ - bidOrder(8, 10, 20, false), - bidOrder(9, 10, 19, false), - }, - expErr: "total ask price \"40peach\" is greater than total bid price \"39peach\"", - }, - { - name: "two asks, two bids: ask partial, total bid price not enough", - askOrders: []*Order{ - askOrder(1, 10, 20, false), - askOrder(2, 10, 20, true), - }, - bidOrders: []*Order{ - bidOrder(8, 10, 20, false), - bidOrder(9, 9, 17, false), - }, - expErr: "total ask price \"38peach\" is greater than total bid price \"37peach\"", - }, - { - name: "two asks, two bids: bid partial, total bid price not enough", - askOrders: []*Order{ - askOrder(1, 10, 25, false), - askOrder(2, 10, 15, false), - }, - bidOrders: []*Order{ - bidOrder(8, 10, 19, false), - bidOrder(9, 11, 22, true), - }, - expErr: "total ask price \"40peach\" is greater than total bid price \"39peach\"", - }, - { - name: "one ask, one bid: both fully filled", - askOrders: []*Order{askOrder(52, 10, 100, false, 2)}, - bidOrders: []*Order{bidOrder(11, 10, 105, false, 3, 4)}, - sellerFeeRatioLookup: ratio(4, 1), - expSettlement: &Settlement{ - Transfers: []*Transfer{ - {Inputs: []banktypes.Input{assetsInput(52, 10)}, Outputs: []banktypes.Output{assetsOutput(11, 10)}}, - {Inputs: []banktypes.Input{priceInput(11, 105)}, Outputs: []banktypes.Output{priceOutput(52, 105)}}, - }, - FeeInputs: []banktypes.Input{ - feeInput("seller52", 29), - feeInput("buyer11", 3, 4), - }, - FullyFilledOrders: []*FilledOrder{ - filled(askOrder(52, 10, 100, false, 2), 105, 29), - filled(bidOrder(11, 10, 105, false, 3, 4), 105, 3, 4), - }, - }, - }, - { - name: "one ask, one bid: ask partially filled", - askOrders: []*Order{askOrder(99, 10, 100, true)}, - bidOrders: []*Order{bidOrder(15, 9, 90, false)}, - expSettlement: &Settlement{ - Transfers: []*Transfer{ - {Inputs: []banktypes.Input{assetsInput(99, 9)}, Outputs: []banktypes.Output{assetsOutput(15, 9)}}, - {Inputs: []banktypes.Input{priceInput(15, 90)}, Outputs: []banktypes.Output{priceOutput(99, 90)}}, - }, - FullyFilledOrders: []*FilledOrder{filled(bidOrder(15, 9, 90, false), 90)}, - PartialOrderFilled: filled(askOrder(99, 9, 90, true), 90), - PartialOrderLeft: askOrder(99, 1, 10, true), - }, - }, - { - name: "one ask, one bid: ask partially filled, not allowed", - askOrders: []*Order{askOrder(99, 10, 100, false)}, - bidOrders: []*Order{bidOrder(15, 9, 90, false)}, - expErr: "cannot split ask order 99 having assets \"10apple\" at \"9apple\": order does not allow partial fulfillment", - }, - { - name: "one ask, one bid: bid partially filled", - askOrders: []*Order{askOrder(8, 9, 85, false, 2)}, - bidOrders: []*Order{bidOrder(12, 10, 100, true)}, - expSettlement: &Settlement{ - Transfers: []*Transfer{ - {Inputs: []banktypes.Input{assetsInput(8, 9)}, Outputs: []banktypes.Output{assetsOutput(12, 9)}}, - {Inputs: []banktypes.Input{priceInput(12, 90)}, Outputs: []banktypes.Output{priceOutput(8, 90)}}, - }, - FeeInputs: []banktypes.Input{feeInput("seller8", 2)}, - FullyFilledOrders: []*FilledOrder{filled(askOrder(8, 9, 85, false, 2), 90, 2)}, - PartialOrderFilled: filled(bidOrder(12, 9, 90, true), 90), - PartialOrderLeft: bidOrder(12, 1, 10, true), - }, - }, - { - name: "one ask, one bid: bid partially filled, not allowed", - askOrders: []*Order{askOrder(8, 9, 85, false, 2)}, - bidOrders: []*Order{bidOrder(12, 10, 100, false)}, - expErr: "cannot split bid order 12 having assets \"10apple\" at \"9apple\": order does not allow partial fulfillment", - }, - { - name: "one ask, five bids: all fully filled", - askOrders: []*Order{askOrder(999, 130, 260, false)}, - bidOrders: []*Order{ - bidOrder(11, 71, 140, false), - bidOrder(12, 10, 20, false, 5), - bidOrder(13, 4, 12, false), - bidOrder(14, 11, 22, false), - bidOrder(15, 34, 68, false, 8), + name: "one ask, five bids: all fully filled", + askOrders: []*Order{askOrder(999, 130, 260, false)}, + bidOrders: []*Order{ + bidOrder(11, 71, 140, false), + bidOrder(12, 10, 20, false, 5), + bidOrder(13, 4, 12, false), + bidOrder(14, 11, 22, false), + bidOrder(15, 34, 68, false, 8), }, sellerFeeRatioLookup: ratio(65, 3), expSettlement: &Settlement{ @@ -3245,9 +1033,7 @@ func TestBuildSettlement(t *testing.T) { bidOrder(44, 130, 1352, true), bidOrder(55, 100, 1000, false, 40, 20), }, - sellerFeeRatioLookup: nil, - expSettlement: nil, - expErr: "", + expErr: "cannot split bid order 55 having assets \"100apple\" at \"95apple\": order does not allow partial fulfillment", }, } @@ -3297,3484 +1083,2749 @@ func TestBuildSettlement(t *testing.T) { } } -func TestValidateCanSettle(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - Assets: assets, - Price: price, - }) +func TestNewIndexedAddrAmts(t *testing.T) { + expected := &indexedAddrAmts{ + addrs: nil, + amts: nil, + indexes: make(map[string]int), } - bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Assets: assets, - Price: price, - }) + actual := newIndexedAddrAmts() + assert.Equal(t, expected, actual, "newIndexedAddrAmts result") + key := "test" + require.NotPanics(t, func() { + _ = actual.indexes[key] + }, "getting value of actual.indexes[%q]", key) +} + +func TestIndexedAddrAmts_Add(t *testing.T) { + coins := func(coins string) sdk.Coins { + rv, err := sdk.ParseCoinsNormalized(coins) + require.NoError(t, err, "sdk.ParseCoinsNormalized(%q)", coins) + return rv } + negCoins := sdk.Coins{sdk.Coin{Denom: "neg", Amount: sdkmath.NewInt(-1)}} tests := []struct { - name string - askOrders []*Order - bidOrders []*Order - expErr string + name string + receiver *indexedAddrAmts + addr string + coins []sdk.Coin + expected *indexedAddrAmts + expPanic string }{ { - name: "nil ask orders", - askOrders: nil, - bidOrders: []*Order{bidOrder(8, coin(10, "apple"), coin(11, "peach"))}, - expErr: "no ask orders provided", + name: "empty, add one coin", + receiver: newIndexedAddrAmts(), + addr: "addr1", + coins: coins("1one"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{"addr1": 0}, + }, }, { - name: "no bid orders", - askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, - bidOrders: nil, - expErr: "no bid orders provided", + name: "empty, add two coins", + receiver: newIndexedAddrAmts(), + addr: "addr1", + coins: coins("1one,2two"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one,2two")}, + indexes: map[string]int{"addr1": 0}, + }, }, { - name: "no orders", - askOrders: nil, - bidOrders: nil, - expErr: joinErrs("no ask orders provided", "no bid orders provided"), + name: "empty, add neg coins", + receiver: newIndexedAddrAmts(), + addr: "addr1", + coins: negCoins, + expPanic: "cannot index and add invalid coin amount \"-1neg\"", }, { - name: "bid order in asks", - askOrders: []*Order{ - askOrder(7, coin(10, "apple"), coin(11, "peach")), - bidOrder(8, coin(10, "apple"), coin(11, "peach")), - askOrder(9, coin(10, "apple"), coin(11, "peach")), + name: "one addr, add to existing new denom", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{"addr1": 0}, + }, + addr: "addr1", + coins: coins("2two"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one,2two")}, + indexes: map[string]int{"addr1": 0}, }, - bidOrders: []*Order{bidOrder(22, coin(10, "apple"), coin(11, "peach"))}, - expErr: "bid order 8 is not an ask order but is in the askOrders list at index 1", }, { - name: "nil inside order in asks", - askOrders: []*Order{ - askOrder(7, coin(10, "apple"), coin(11, "peach")), - NewOrder(8), - askOrder(9, coin(10, "apple"), coin(11, "peach")), + name: "one addr, add to existing same denom", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{"addr1": 0}, + }, + addr: "addr1", + coins: coins("3one"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("4one")}, + indexes: map[string]int{"addr1": 0}, }, - bidOrders: []*Order{bidOrder(22, coin(10, "apple"), coin(11, "peach"))}, - expErr: " order 8 is not an ask order but is in the askOrders list at index 1", }, { - name: "unknown inside order in asks", - askOrders: []*Order{ - askOrder(7, coin(10, "apple"), coin(11, "peach")), - newUnknownOrder(8), - askOrder(9, coin(10, "apple"), coin(11, "peach")), + name: "one addr, add negative to existing", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{"addr1": 0}, + }, + addr: "addr1", + coins: negCoins, + expPanic: "cannot index and add invalid coin amount \"-1neg\"", + }, + { + name: "one addr, add to new", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{"addr1": 0}, + }, + addr: "addr2", + coins: coins("2two"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2"}, + amts: []sdk.Coins{coins("1one"), coins("2two")}, + indexes: map[string]int{"addr1": 0, "addr2": 1}, + }, + }, + { + name: "one addr, add to new opposite order", + receiver: &indexedAddrAmts{ + addrs: []string{"addr2"}, + amts: []sdk.Coins{coins("2two")}, + indexes: map[string]int{"addr2": 0}, + }, + addr: "addr1", + coins: coins("1one"), + expected: &indexedAddrAmts{ + addrs: []string{"addr2", "addr1"}, + amts: []sdk.Coins{coins("2two"), coins("1one")}, + indexes: map[string]int{"addr2": 0, "addr1": 1}, + }, + }, + { + name: "one addr, add negative to new", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{"addr1": 0}, + }, + addr: "addr2", + coins: negCoins, + expPanic: "cannot index and add invalid coin amount \"-1neg\"", + }, + { + name: "three addrs, add to first", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + }, + addr: "addr1", + coins: coins("10one"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("11one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + }, + }, + { + name: "three addrs, add to second", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + }, + addr: "addr2", + coins: coins("10two"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("12two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, }, - bidOrders: []*Order{bidOrder(22, coin(10, "apple"), coin(11, "peach"))}, - expErr: "*exchange.unknownOrderType order 8 is not an ask order but is in the askOrders list at index 1", }, { - name: "ask order in bids", - askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, - bidOrders: []*Order{ - bidOrder(21, coin(10, "apple"), coin(11, "peach")), - askOrder(22, coin(10, "apple"), coin(11, "peach")), - bidOrder(23, coin(10, "apple"), coin(11, "peach")), + name: "three addrs, add to third", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, }, - expErr: "ask order 22 is not a bid order but is in the bidOrders list at index 1", - }, - { - name: "nil inside order in bids", - askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, - bidOrders: []*Order{ - bidOrder(21, coin(10, "apple"), coin(11, "peach")), - NewOrder(22), - bidOrder(23, coin(10, "apple"), coin(11, "peach")), + addr: "addr3", + coins: coins("10three"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("13three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, }, - expErr: " order 22 is not a bid order but is in the bidOrders list at index 1", }, { - name: "unknown inside order in bids", - askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, - bidOrders: []*Order{ - bidOrder(21, coin(10, "apple"), coin(11, "peach")), - newUnknownOrder(22), - bidOrder(23, coin(10, "apple"), coin(11, "peach")), + name: "three addrs, add two coins to second", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + }, + addr: "addr2", + coins: coins("10four,20two"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("10four,22two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, }, - expErr: "*exchange.unknownOrderType order 22 is not a bid order but is in the bidOrders list at index 1", }, { - name: "orders in wrong args", - askOrders: []*Order{ - askOrder(15, coin(10, "apple"), coin(11, "peach")), - bidOrder(16, coin(10, "apple"), coin(11, "peach")), - askOrder(17, coin(10, "apple"), coin(11, "peach")), + name: "three addrs, add to new", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, }, - bidOrders: []*Order{ - bidOrder(91, coin(10, "apple"), coin(11, "peach")), - askOrder(92, coin(10, "apple"), coin(11, "peach")), - bidOrder(93, coin(10, "apple"), coin(11, "peach")), + addr: "good buddy", + coins: coins("10four"), + expected: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3", "good buddy"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three"), coins("10four")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2, "good buddy": 3}, }, - expErr: joinErrs( - "bid order 16 is not an ask order but is in the askOrders list at index 1", - "ask order 92 is not a bid order but is in the bidOrders list at index 1", - ), }, { - name: "multiple ask asset denoms", - askOrders: []*Order{ - askOrder(55, coin(10, "apple"), coin(11, "peach")), - askOrder(56, coin(20, "avocado"), coin(22, "peach")), - }, - bidOrders: []*Order{ - bidOrder(61, coin(10, "apple"), coin(11, "peach")), + name: "three addrs, add negative to second", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, }, - expErr: "cannot settle with multiple ask order asset denoms \"10apple,20avocado\"", + addr: "addr2", + coins: negCoins, + expPanic: "cannot index and add invalid coin amount \"-1neg\"", }, { - name: "multiple ask price denoms", - askOrders: []*Order{ - askOrder(55, coin(10, "apple"), coin(11, "peach")), - askOrder(56, coin(20, "apple"), coin(22, "plum")), - }, - bidOrders: []*Order{ - bidOrder(61, coin(10, "apple"), coin(11, "peach")), + name: "three addrs, add negative to new", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, + indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, }, - expErr: "cannot settle with multiple ask order price denoms \"11peach,22plum\"", + addr: "addr4", + coins: negCoins, + expPanic: "cannot index and add invalid coin amount \"-1neg\"", }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + orig := copyIndexedAddrAmts(tc.receiver) + defer func() { + if t.Failed() { + t.Logf("Original: %s", indexedAddrAmtsString(orig)) + t.Logf(" Actual: %s", indexedAddrAmtsString(tc.receiver)) + t.Logf("Expected: %s", indexedAddrAmtsString(tc.expected)) + } + }() + + testFunc := func() { + tc.receiver.add(tc.addr, tc.coins...) + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "add(%q, %q)", tc.addr, tc.coins) + if len(tc.expPanic) == 0 { + assert.Equal(t, tc.expected, tc.receiver, "receiver after add(%q, %q)", tc.addr, tc.coins) + } + }) + } +} + +func TestIndexedAddrAmts_GetAsInputs(t *testing.T) { + coins := func(coins string) sdk.Coins { + rv, err := sdk.ParseCoinsNormalized(coins) + require.NoError(t, err, "sdk.ParseCoinsNormalized(%q)", coins) + return rv + } + + tests := []struct { + name string + receiver *indexedAddrAmts + expected []banktypes.Input + expPanic string + }{ + {name: "nil receiver", receiver: nil, expected: nil}, + {name: "no addrs", receiver: newIndexedAddrAmts(), expected: nil}, { - name: "multiple bid asset denoms", - askOrders: []*Order{askOrder(88, coin(10, "apple"), coin(11, "peach"))}, - bidOrders: []*Order{ - bidOrder(12, coin(10, "apple"), coin(11, "peach")), - bidOrder(13, coin(20, "avocado"), coin(22, "peach")), + name: "one addr negative amount", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{{{Denom: "neg", Amount: sdkmath.NewInt(-1)}}}, + indexes: map[string]int{ + "addr1": 0, + }, }, - expErr: "cannot settle with multiple bid order asset denoms \"10apple,20avocado\"", + expPanic: "invalid indexed amount \"addr1\" for address \"-1neg\": cannot be zero or negative", }, { - name: "multiple bid price denoms", - askOrders: []*Order{askOrder(88, coin(10, "apple"), coin(11, "peach"))}, - bidOrders: []*Order{ - bidOrder(12, coin(10, "apple"), coin(11, "peach")), - bidOrder(13, coin(20, "apple"), coin(22, "plum")), + name: "one addr zero amount", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{{{Denom: "zero", Amount: sdkmath.NewInt(0)}}}, + indexes: map[string]int{ + "addr1": 0, + }, }, - expErr: "cannot settle with multiple bid order price denoms \"11peach,22plum\"", + expPanic: "invalid indexed amount \"addr1\" for address \"0zero\": cannot be zero or negative", }, { - name: "all different denoms", - askOrders: []*Order{ - askOrder(55, coin(10, "apple"), coin(11, "peach")), - askOrder(56, coin(20, "avocado"), coin(22, "plum")), + name: "one addr positive amount", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{ + "addr1": 0, + }, }, - bidOrders: []*Order{ - bidOrder(12, coin(30, "acorn"), coin(33, "prune")), - bidOrder(13, coin(40, "acai"), coin(44, "pear")), + expected: []banktypes.Input{ + {Address: "addr1", Coins: coins("1one")}, }, - expErr: joinErrs( - "cannot settle with multiple ask order asset denoms \"10apple,20avocado\"", - "cannot settle with multiple ask order price denoms \"11peach,22plum\"", - "cannot settle with multiple bid order asset denoms \"40acai,30acorn\"", - "cannot settle with multiple bid order price denoms \"44pear,33prune\"", - ), }, { - name: "different ask and bid asset denoms", - askOrders: []*Order{ - askOrder(15, coin(10, "apple"), coin(11, "peach")), - askOrder(16, coin(20, "apple"), coin(22, "peach")), + name: "two addrs", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2"}, + amts: []sdk.Coins{coins("1one"), coins("2two,3three")}, + indexes: map[string]int{ + "addr1": 0, + "addr2": 1, + }, }, - bidOrders: []*Order{ - bidOrder(2001, coin(30, "acorn"), coin(33, "peach")), - bidOrder(2002, coin(40, "acorn"), coin(44, "peach")), + expected: []banktypes.Input{ + {Address: "addr1", Coins: coins("1one")}, + {Address: "addr2", Coins: coins("2two,3three")}, }, - expErr: "cannot settle different ask \"30apple\" and bid \"70acorn\" asset denoms", }, { - name: "different ask and bid price denoms", - askOrders: []*Order{ - askOrder(15, coin(10, "apple"), coin(11, "peach")), - askOrder(16, coin(20, "apple"), coin(22, "peach")), + name: "three addrs", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two,3three"), coins("4four,5five,6six")}, + indexes: map[string]int{ + "addr1": 0, + "addr2": 1, + "addr3": 2, + }, }, - bidOrders: []*Order{ - bidOrder(2001, coin(30, "apple"), coin(33, "plum")), - bidOrder(2002, coin(40, "apple"), coin(44, "plum")), + expected: []banktypes.Input{ + {Address: "addr1", Coins: coins("1one")}, + {Address: "addr2", Coins: coins("2two,3three")}, + {Address: "addr3", Coins: coins("4four,5five,6six")}, }, - expErr: "cannot settle different ask \"33peach\" and bid \"77plum\" price denoms", }, { - name: "different ask and bid denoms", - askOrders: []*Order{ - askOrder(15, coin(10, "apple"), coin(11, "peach")), - askOrder(16, coin(20, "apple"), coin(22, "peach")), - }, - bidOrders: []*Order{ - bidOrder(2001, coin(30, "acorn"), coin(33, "plum")), - bidOrder(2002, coin(40, "acorn"), coin(44, "plum")), + name: "three addrs, negative in third", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{ + coins("1one"), + coins("2two,3three"), + { + {Denom: "acoin", Amount: sdkmath.NewInt(4)}, + {Denom: "bcoin", Amount: sdkmath.NewInt(5)}, + {Denom: "ncoin", Amount: sdkmath.NewInt(-6)}, + {Denom: "zcoin", Amount: sdkmath.NewInt(7)}, + }, + }, + indexes: map[string]int{ + "addr1": 0, + "addr2": 1, + "addr3": 2, + }, }, - expErr: joinErrs( - "cannot settle different ask \"30apple\" and bid \"70acorn\" asset denoms", - "cannot settle different ask \"33peach\" and bid \"77plum\" price denoms", - ), + expPanic: "invalid indexed amount \"addr3\" for address \"4acoin,5bcoin,-6ncoin,7zcoin\": cannot be zero or negative", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var err error + orig := copyIndexedAddrAmts(tc.receiver) + var actual []banktypes.Input testFunc := func() { - err = validateCanSettle(tc.askOrders, tc.bidOrders) + actual = tc.receiver.getAsInputs() + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getAsInputs()") + assert.Equal(t, tc.expected, actual, "getAsInputs() result") + if !assert.Equal(t, orig, tc.receiver, "receiver before and after getAsInputs()") { + t.Logf("Before: %s", indexedAddrAmtsString(orig)) + t.Logf(" After: %s", indexedAddrAmtsString(tc.receiver)) } - require.NotPanics(t, testFunc, "validateCanSettle") - assertions.AssertErrorValue(t, err, tc.expErr, "validateCanSettle error") }) } } -func TestAllocateAssets(t *testing.T) { - askOrder := func(orderID uint64, assetsAmt int64, seller string) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - Seller: seller, - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, - }) - } - bidOrder := func(orderID uint64, assetsAmt int64, buyer string) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Buyer: buyer, - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, - }) - } - newOF := func(order *Order, dists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: order.GetAssets().Amount, - } - if len(dists) > 0 { - rv.AssetDists = dists - for _, d := range dists { - rv.AssetsFilledAmt = rv.AssetsFilledAmt.Add(d.Amount) - rv.AssetsUnfilledAmt = rv.AssetsUnfilledAmt.Sub(d.Amount) - } - } +func TestIndexedAddrAmts_GetAsOutputs(t *testing.T) { + coins := func(coins string) sdk.Coins { + rv, err := sdk.ParseCoinsNormalized(coins) + require.NoError(t, err, "sdk.ParseCoinsNormalized(%q)", coins) return rv } - dist := func(addr string, amount int64) *Distribution { - return &Distribution{Address: addr, Amount: sdkmath.NewInt(amount)} - } tests := []struct { - name string - askOFs []*OrderFulfillment - bidOFs []*OrderFulfillment - expAskOFs []*OrderFulfillment - expBidOfs []*OrderFulfillment - expErr string + name string + receiver *indexedAddrAmts + expected []banktypes.Output + expPanic string }{ + {name: "nil receiver", receiver: nil, expected: nil}, + {name: "no addrs", receiver: newIndexedAddrAmts(), expected: nil}, { - name: "one ask, one bid: both full", - askOFs: []*OrderFulfillment{newOF(askOrder(5, 10, "seller"))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(6, 10, "buyer"))}, - expAskOFs: []*OrderFulfillment{newOF(askOrder(5, 10, "seller"), dist("buyer", 10))}, - expBidOfs: []*OrderFulfillment{newOF(bidOrder(6, 10, "buyer"), dist("seller", 10))}, - }, - { - name: "one ask, one bid: ask partial", - askOFs: []*OrderFulfillment{newOF(askOrder(5, 11, "seller"))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(16, 10, "buyer"))}, - expAskOFs: []*OrderFulfillment{newOF(askOrder(5, 11, "seller"), dist("buyer", 10))}, - expBidOfs: []*OrderFulfillment{newOF(bidOrder(16, 10, "buyer"), dist("seller", 10))}, + name: "one addr negative amount", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{{{Denom: "neg", Amount: sdkmath.NewInt(-1)}}}, + indexes: map[string]int{ + "addr1": 0, + }, + }, + expPanic: "invalid indexed amount \"addr1\" for address \"-1neg\": cannot be zero or negative", }, { - name: "one ask, one bid: bid partial", - askOFs: []*OrderFulfillment{newOF(askOrder(15, 10, "seller"))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(6, 11, "buyer"))}, - expAskOFs: []*OrderFulfillment{newOF(askOrder(15, 10, "seller"), dist("buyer", 10))}, - expBidOfs: []*OrderFulfillment{newOF(bidOrder(6, 11, "buyer"), dist("seller", 10))}, + name: "one addr zero amount", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{{{Denom: "zero", Amount: sdkmath.NewInt(0)}}}, + indexes: map[string]int{ + "addr1": 0, + }, + }, + expPanic: "invalid indexed amount \"addr1\" for address \"0zero\": cannot be zero or negative", }, { - name: "one ask, two bids: last bid not touched", - askOFs: []*OrderFulfillment{newOF(askOrder(22, 10, "seller"))}, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(64, 12, "buyer64")), - newOF(bidOrder(78, 1, "buyer78")), + name: "one addr positive amount", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1"}, + amts: []sdk.Coins{coins("1one")}, + indexes: map[string]int{ + "addr1": 0, + }, }, - expAskOFs: []*OrderFulfillment{newOF(askOrder(22, 10, "seller"), dist("buyer64", 10))}, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(64, 12, "buyer64"), dist("seller", 10)), - newOF(bidOrder(78, 1, "buyer78")), + expected: []banktypes.Output{ + {Address: "addr1", Coins: coins("1one")}, }, }, { - name: "two asks, one bids: last ask not touched", - askOFs: []*OrderFulfillment{ - newOF(askOrder(888, 10, "seller888")), - newOF(askOrder(999, 10, "seller999")), + name: "two addrs", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2"}, + amts: []sdk.Coins{coins("1one"), coins("2two,3three")}, + indexes: map[string]int{ + "addr1": 0, + "addr2": 1, + }, }, - bidOFs: []*OrderFulfillment{newOF(bidOrder(6, 10, "buyer"))}, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(888, 10, "seller888"), dist("buyer", 10)), - newOF(askOrder(999, 10, "seller999")), + expected: []banktypes.Output{ + {Address: "addr1", Coins: coins("1one")}, + {Address: "addr2", Coins: coins("2two,3three")}, }, - expBidOfs: []*OrderFulfillment{newOF(bidOrder(6, 10, "buyer"), dist("seller888", 10))}, }, { - name: "two asks, three bids: both full", - askOFs: []*OrderFulfillment{ - newOF(askOrder(101, 15, "seller101")), - newOF(askOrder(102, 25, "seller102")), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(103, 10, "buyer103")), - newOF(bidOrder(104, 8, "buyer104")), - newOF(bidOrder(105, 22, "buyer105")), - }, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(101, 15, "seller101"), dist("buyer103", 10), dist("buyer104", 5)), - newOF(askOrder(102, 25, "seller102"), dist("buyer104", 3), dist("buyer105", 22)), + name: "three addrs", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{coins("1one"), coins("2two,3three"), coins("4four,5five,6six")}, + indexes: map[string]int{ + "addr1": 0, + "addr2": 1, + "addr3": 2, + }, }, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(103, 10, "buyer103"), dist("seller101", 10)), - newOF(bidOrder(104, 8, "buyer104"), dist("seller101", 5), dist("seller102", 3)), - newOF(bidOrder(105, 22, "buyer105"), dist("seller102", 22)), + expected: []banktypes.Output{ + {Address: "addr1", Coins: coins("1one")}, + {Address: "addr2", Coins: coins("2two,3three")}, + {Address: "addr3", Coins: coins("4four,5five,6six")}, }, }, { - name: "two asks, three bids: ask partial", - askOFs: []*OrderFulfillment{ - newOF(askOrder(101, 15, "seller101")), - newOF(askOrder(102, 26, "seller102")), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(103, 10, "buyer103")), - newOF(bidOrder(104, 8, "buyer104")), - newOF(bidOrder(105, 22, "buyer105")), - }, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(101, 15, "seller101"), dist("buyer103", 10), dist("buyer104", 5)), - newOF(askOrder(102, 26, "seller102"), dist("buyer104", 3), dist("buyer105", 22)), - }, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(103, 10, "buyer103"), dist("seller101", 10)), - newOF(bidOrder(104, 8, "buyer104"), dist("seller101", 5), dist("seller102", 3)), - newOF(bidOrder(105, 22, "buyer105"), dist("seller102", 22)), + name: "three addrs, negative in third", + receiver: &indexedAddrAmts{ + addrs: []string{"addr1", "addr2", "addr3"}, + amts: []sdk.Coins{ + coins("1one"), + coins("2two,3three"), + { + {Denom: "acoin", Amount: sdkmath.NewInt(4)}, + {Denom: "bcoin", Amount: sdkmath.NewInt(5)}, + {Denom: "ncoin", Amount: sdkmath.NewInt(-6)}, + {Denom: "zcoin", Amount: sdkmath.NewInt(7)}, + }, + }, + indexes: map[string]int{ + "addr1": 0, + "addr2": 1, + "addr3": 2, + }, }, + expPanic: "invalid indexed amount \"addr3\" for address \"4acoin,5bcoin,-6ncoin,7zcoin\": cannot be zero or negative", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + orig := copyIndexedAddrAmts(tc.receiver) + var actual []banktypes.Output + testFunc := func() { + actual = tc.receiver.getAsOutputs() + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getAsOutputs()") + assert.Equal(t, tc.expected, actual, "getAsOutputs() result") + if !assert.Equal(t, orig, tc.receiver, "receiver before and after getAsInputs()") { + t.Logf("Before: %s", indexedAddrAmtsString(orig)) + t.Logf(" After: %s", indexedAddrAmtsString(tc.receiver)) + } + }) + } +} + +func TestNewOrderFulfillment(t *testing.T) { + tests := []struct { + name string + order *Order + expected *orderFulfillment + expPanic string + }{ + { + name: "nil sub-order", + order: NewOrder(1), + expPanic: nilSubTypeErr(1), }, { - name: "two asks, three bids: bid partial", - askOFs: []*OrderFulfillment{ - newOF(askOrder(101, 15, "seller101")), - newOF(askOrder(102, 25, "seller102")), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(103, 10, "buyer103")), - newOF(bidOrder(104, 8, "buyer104")), - newOF(bidOrder(105, 23, "buyer105")), - }, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(101, 15, "seller101"), dist("buyer103", 10), dist("buyer104", 5)), - newOF(askOrder(102, 25, "seller102"), dist("buyer104", 3), dist("buyer105", 22)), + name: "ask order", + order: NewOrder(2).WithAsk(&AskOrder{ + MarketId: 10, + Assets: sdk.NewInt64Coin("adolla", 92), + Price: sdk.NewInt64Coin("pdolla", 15), + }), + expected: &orderFulfillment{ + Order: &Order{ + OrderId: 2, + Order: &Order_AskOrder{ + AskOrder: &AskOrder{ + MarketId: 10, + Assets: sdk.NewInt64Coin("adolla", 92), + Price: sdk.NewInt64Coin("pdolla", 15), + }, + }, + }, + AssetsFilledAmt: sdkmath.ZeroInt(), + AssetsUnfilledAmt: sdkmath.NewInt(92), + PriceAppliedAmt: sdkmath.ZeroInt(), + PriceLeftAmt: sdkmath.NewInt(15), + FeesToPay: nil, }, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(103, 10, "buyer103"), dist("seller101", 10)), - newOF(bidOrder(104, 8, "buyer104"), dist("seller101", 5), dist("seller102", 3)), - newOF(bidOrder(105, 23, "buyer105"), dist("seller102", 22)), + }, + { + name: "bid order", + order: NewOrder(3).WithBid(&BidOrder{ + MarketId: 11, + Assets: sdk.NewInt64Coin("adolla", 93), + Price: sdk.NewInt64Coin("pdolla", 16), + }), + expected: &orderFulfillment{ + Order: &Order{ + OrderId: 3, + Order: &Order_BidOrder{ + BidOrder: &BidOrder{ + MarketId: 11, + Assets: sdk.NewInt64Coin("adolla", 93), + Price: sdk.NewInt64Coin("pdolla", 16), + }, + }, + }, + AssetsFilledAmt: sdkmath.ZeroInt(), + AssetsUnfilledAmt: sdkmath.NewInt(93), + PriceAppliedAmt: sdkmath.ZeroInt(), + PriceLeftAmt: sdkmath.NewInt(16), + FeesToPay: nil, }, }, - { - name: "negative ask assets unfilled", - askOFs: []*OrderFulfillment{newOF(askOrder(101, 10, "seller"), dist("buyerx", 11))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(102, 10, "buyer"))}, - expErr: "cannot fill ask order 101 having assets left \"-1apple\" with bid order 102 having " + - "assets left \"10apple\": zero or negative assets left", - }, - { - name: "negative bid assets unfilled", - askOFs: []*OrderFulfillment{newOF(askOrder(101, 10, "seller"))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(102, 10, "buyer"), dist("sellerx", 11))}, - expErr: "cannot fill ask order 101 having assets left \"10apple\" with bid order 102 having " + - "assets left \"-1apple\": zero or negative assets left", - }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - origAskOFs := copyOrderFulfillments(tc.askOFs) - origBidOFs := copyOrderFulfillments(tc.bidOFs) + var actual *orderFulfillment + defer func() { + if t.Failed() { + t.Logf(" Actual: %s", orderFulfillmentString(actual)) + t.Logf("Expected: %s", orderFulfillmentString(tc.expected)) + } + }() - var err error testFunc := func() { - err = allocateAssets(tc.askOFs, tc.bidOFs) - } - require.NotPanics(t, testFunc, "allocateAssets") - assertions.AssertErrorValue(t, err, tc.expErr, "allocateAssets error") - if len(tc.expErr) > 0 { - return - } - if !assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after allocateAssets") { - t.Logf("Original: %s", orderFulfillmentsString(origAskOFs)) - } - if !assertEqualOrderFulfillmentSlices(t, tc.expBidOfs, tc.bidOFs, "bidOFs after allocateAssets") { - t.Logf("Original: %s", orderFulfillmentsString(origBidOFs)) + actual = newOrderFulfillment(tc.order) } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "newOrderFulfillment") + assert.Equal(t, tc.expected, actual, "newOrderFulfillment result") }) } } -func TestSplitPartial(t *testing.T) { - askOrder := func(orderID uint64, assetsAmt int64, seller string) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - Seller: seller, - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, - Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(assetsAmt)}, - AllowPartial: true, - }) +func TestNewOrderFulfillments(t *testing.T) { + assetCoin := func(amount int64) sdk.Coin { + return sdk.NewInt64Coin("anise", amount) } - bidOrder := func(orderID uint64, assetsAmt int64, buyer string) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Buyer: buyer, - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, - Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(assetsAmt)}, - AllowPartial: true, - }) + priceCoin := func(amount int64) sdk.Coin { + return sdk.NewInt64Coin("paprika", amount) } - newOF := func(order *Order, dists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: order.GetAssets().Amount, - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: order.GetPrice().Amount, + feeCoin := func(amount int64) *sdk.Coin { + rv := sdk.NewInt64Coin("fennel", amount) + return &rv + } + + askOrders := make([]*Order, 4) // ids 1, 2, 3, 4 + for j := range askOrders { + i := int64(j) + 1 + order := &AskOrder{ + MarketId: uint32(90 + i), + Seller: fmt.Sprintf("seller-%d", i), + Assets: assetCoin(1000*i + 100*i + 10*i + i), + Price: priceCoin(100*i + 10*i + i), } - if len(dists) > 0 { - rv.AssetDists = dists - for _, d := range dists { - rv.AssetsFilledAmt = rv.AssetsFilledAmt.Add(d.Amount) - rv.AssetsUnfilledAmt = rv.AssetsUnfilledAmt.Sub(d.Amount) - } - if rv.AssetsUnfilledAmt.IsZero() { - rv.AssetsUnfilledAmt = sdkmath.NewInt(0) - } + if j%2 == 0 { + order.SellerSettlementFlatFee = feeCoin(10*i + i) } - return rv + if j >= 2 { + order.AllowPartial = true + } + askOrders[j] = NewOrder(uint64(i)).WithAsk(order) } - dist := func(addr string, amount int64) *Distribution { - return &Distribution{Address: addr, Amount: sdkmath.NewInt(amount)} + + bidOrders := make([]*Order, 4) // ids 5, 6, 7, 8 + for j := range bidOrders { + i := int64(j + 5) + order := &BidOrder{ + MarketId: uint32(90 + i), + Buyer: fmt.Sprintf("buyer-%d", i), + Assets: assetCoin(1000*i + 100*i + 10*i + i), + Price: priceCoin(100*i + 10*i + i), + } + switch j { + case 0: + order.BuyerSettlementFees = sdk.Coins{*feeCoin(10*i + i)} + case 2: + order.BuyerSettlementFees = sdk.Coins{ + *feeCoin(10*i + i), + sdk.NewInt64Coin("garlic", 10000*i+1000*i+100*i+10*i+i), + } + } + if j >= 2 { + order.AllowPartial = true + } + bidOrders[j] = NewOrder(uint64(i)).WithBid(order) } tests := []struct { - name string - askOFs []*OrderFulfillment - bidOFs []*OrderFulfillment - settlement *Settlement - expAskOFs []*OrderFulfillment - expBidOfs []*OrderFulfillment - expSettlement *Settlement - expErr string + name string + orders []*Order + expected []*orderFulfillment }{ { - name: "one ask: not touched", - askOFs: []*OrderFulfillment{newOF(askOrder(8, 10, "seller8"))}, - settlement: &Settlement{}, - expErr: "ask order 8 (at index 0) has no assets filled", - }, - { - name: "one ask: partial", - askOFs: []*OrderFulfillment{newOF(askOrder(8, 10, "seller8"), dist("buyer", 7))}, - settlement: &Settlement{}, - expAskOFs: []*OrderFulfillment{newOF(askOrder(8, 7, "seller8"), dist("buyer", 7))}, - expSettlement: &Settlement{PartialOrderLeft: askOrder(8, 3, "seller8")}, - }, - { - name: "one ask: partial, settlement already has a partial", - askOFs: []*OrderFulfillment{newOF(askOrder(8, 10, "seller8"), dist("buyer", 7))}, - settlement: &Settlement{PartialOrderLeft: bidOrder(55, 3, "buyer")}, - expErr: "bid order 55 and ask order 8 cannot both be partially filled", + name: "nil orders", + orders: nil, + expected: []*orderFulfillment{}, }, { - name: "one ask: partial, not allowed", - askOFs: []*OrderFulfillment{ - newOF(NewOrder(8).WithAsk(&AskOrder{ - Seller: "seller8", - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(10)}, - Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(10)}, - AllowPartial: false, - }), dist("buyer", 7))}, - settlement: &Settlement{}, - expErr: "cannot split ask order 8 having assets \"10apple\" at \"7apple\": order does not allow partial fulfillment", + name: "empty orders", + orders: []*Order{}, + expected: []*orderFulfillment{}, }, { - name: "two asks: first partial", - askOFs: []*OrderFulfillment{ - newOF(askOrder(8, 10, "seller8"), dist("buyer", 7)), - newOF(askOrder(9, 12, "seller8")), - }, - settlement: &Settlement{}, - expErr: "ask order 8 (at index 0) is not filled in full and is not the last ask order provided", + name: "1 ask order", + orders: []*Order{askOrders[0]}, + expected: []*orderFulfillment{newOrderFulfillment(askOrders[0])}, }, { - name: "two asks: last untouched", - askOFs: []*OrderFulfillment{ - newOF(askOrder(8, 10, "seller8"), dist("buyer", 10)), - newOF(askOrder(9, 12, "seller8")), - }, - settlement: &Settlement{}, - expErr: "ask order 9 (at index 1) has no assets filled", + name: "1 bid order", + orders: []*Order{bidOrders[0]}, + expected: []*orderFulfillment{newOrderFulfillment(bidOrders[0])}, }, { - name: "two asks: last partial", - askOFs: []*OrderFulfillment{ - newOF(askOrder(8, 10, "seller8"), dist("buyer", 10)), - newOF(askOrder(9, 12, "seller9"), dist("buyer", 10)), - }, - settlement: &Settlement{}, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(8, 10, "seller8"), dist("buyer", 10)), - newOF(askOrder(9, 10, "seller9"), dist("buyer", 10)), + name: "4 ask orders", + orders: []*Order{askOrders[0], askOrders[1], askOrders[2], askOrders[3]}, + expected: []*orderFulfillment{ + newOrderFulfillment(askOrders[0]), + newOrderFulfillment(askOrders[1]), + newOrderFulfillment(askOrders[2]), + newOrderFulfillment(askOrders[3]), }, - expSettlement: &Settlement{PartialOrderLeft: askOrder(9, 2, "seller9")}, - }, - - { - name: "one bid: not touched", - bidOFs: []*OrderFulfillment{newOF(bidOrder(8, 10, "buyer8"))}, - settlement: &Settlement{}, - expErr: "bid order 8 (at index 0) has no assets filled", - }, - { - name: "one bid: partial", - bidOFs: []*OrderFulfillment{newOF(bidOrder(8, 10, "buyer8"), dist("seller", 7))}, - settlement: &Settlement{}, - expBidOfs: []*OrderFulfillment{newOF(bidOrder(8, 7, "buyer8"), dist("seller", 7))}, - expSettlement: &Settlement{PartialOrderLeft: bidOrder(8, 3, "buyer8")}, - }, - { - name: "one bid: partial, not allowed", - askOFs: []*OrderFulfillment{ - newOF(NewOrder(8).WithBid(&BidOrder{ - Buyer: "buyer8", - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(10)}, - Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(10)}, - AllowPartial: false, - }), dist("seller", 7))}, - settlement: &Settlement{}, - expErr: "cannot split bid order 8 having assets \"10apple\" at \"7apple\": order does not allow partial fulfillment", }, { - name: "two bids: first partial", - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(8, 10, "buyer8"), dist("seller", 7)), - newOF(bidOrder(9, 12, "buyer9")), + name: "4 bid orders", + orders: []*Order{bidOrders[0], bidOrders[1], bidOrders[2], bidOrders[3]}, + expected: []*orderFulfillment{ + newOrderFulfillment(bidOrders[0]), + newOrderFulfillment(bidOrders[1]), + newOrderFulfillment(bidOrders[2]), + newOrderFulfillment(bidOrders[3]), }, - settlement: &Settlement{}, - expErr: "bid order 8 (at index 0) is not filled in full and is not the last bid order provided", }, { - name: "two bids: last untouched", - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(8, 10, "buyer8"), dist("seller", 10)), - newOF(bidOrder(9, 12, "buyer9")), + name: "1 bid 1 ask", + orders: []*Order{askOrders[1], bidOrders[2]}, + expected: []*orderFulfillment{ + newOrderFulfillment(askOrders[1]), + newOrderFulfillment(bidOrders[2]), }, - settlement: &Settlement{}, - expErr: "bid order 9 (at index 1) has no assets filled", }, { - name: "two bids: last partial", - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(8, 10, "buyer8"), dist("seller", 10)), - newOF(bidOrder(9, 12, "buyer9"), dist("seller", 10)), - }, - settlement: &Settlement{}, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(8, 10, "buyer8"), dist("seller", 10)), - newOF(bidOrder(9, 10, "buyer9"), dist("seller", 10)), + name: "1 ask 1 bid", + orders: []*Order{bidOrders[1], askOrders[2]}, + expected: []*orderFulfillment{ + newOrderFulfillment(bidOrders[1]), + newOrderFulfillment(askOrders[2]), }, - expSettlement: &Settlement{PartialOrderLeft: bidOrder(9, 2, "buyer9")}, - }, - { - name: "one ask, one bid: both partial", - askOFs: []*OrderFulfillment{newOF(askOrder(8, 10, "seller8"), dist("buyer9", 7))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(9, 10, "buyer9"), dist("seller8", 7))}, - settlement: &Settlement{}, - expErr: "ask order 8 and bid order 9 cannot both be partially filled", }, { - name: "three asks, three bids: no partial", - askOFs: []*OrderFulfillment{ - newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), - newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), - newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), - newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), - newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), - }, - settlement: &Settlement{}, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), - newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), - newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), + name: "4 asks 4 bids", + orders: []*Order{ + askOrders[0], askOrders[1], askOrders[2], askOrders[3], + bidOrders[3], bidOrders[2], bidOrders[1], bidOrders[0], }, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), - newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), - newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), + expected: []*orderFulfillment{ + newOrderFulfillment(askOrders[0]), + newOrderFulfillment(askOrders[1]), + newOrderFulfillment(askOrders[2]), + newOrderFulfillment(askOrders[3]), + newOrderFulfillment(bidOrders[3]), + newOrderFulfillment(bidOrders[2]), + newOrderFulfillment(bidOrders[1]), + newOrderFulfillment(bidOrders[0]), }, - expSettlement: &Settlement{PartialOrderLeft: nil}, }, { - name: "three asks, three bids: partial ask", - askOFs: []*OrderFulfillment{ - newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), - newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), - newOF(askOrder(12, 21, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), - newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), - newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), - }, - settlement: &Settlement{}, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), - newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), - newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), + name: "4 bids 4 asks", + orders: []*Order{ + bidOrders[0], bidOrders[1], bidOrders[2], bidOrders[3], + askOrders[3], askOrders[2], askOrders[1], askOrders[0], }, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), - newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), - newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), + expected: []*orderFulfillment{ + newOrderFulfillment(bidOrders[0]), + newOrderFulfillment(bidOrders[1]), + newOrderFulfillment(bidOrders[2]), + newOrderFulfillment(bidOrders[3]), + newOrderFulfillment(askOrders[3]), + newOrderFulfillment(askOrders[2]), + newOrderFulfillment(askOrders[1]), + newOrderFulfillment(askOrders[0]), }, - expSettlement: &Settlement{PartialOrderLeft: askOrder(12, 1, "seller12")}, }, { - name: "three asks, three bids: no partial", - askOFs: []*OrderFulfillment{ - newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), - newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), - newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), + name: "interweaved 4 asks 4 bids", + orders: []*Order{ + bidOrders[3], askOrders[0], askOrders[3], bidOrders[1], + bidOrders[0], askOrders[1], bidOrders[2], askOrders[2], }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), - newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), - newOF(bidOrder(112, 11, "buyer112"), dist("seller12", 10)), + expected: []*orderFulfillment{ + newOrderFulfillment(bidOrders[3]), + newOrderFulfillment(askOrders[0]), + newOrderFulfillment(askOrders[3]), + newOrderFulfillment(bidOrders[1]), + newOrderFulfillment(bidOrders[0]), + newOrderFulfillment(askOrders[1]), + newOrderFulfillment(bidOrders[2]), + newOrderFulfillment(askOrders[2]), }, - settlement: &Settlement{}, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), - newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), - newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), + }, + { + name: "duplicated entries", + orders: []*Order{ + askOrders[3], bidOrders[2], askOrders[3], bidOrders[2], }, - expBidOfs: []*OrderFulfillment{ - newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), - newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), - newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), + expected: []*orderFulfillment{ + newOrderFulfillment(askOrders[3]), + newOrderFulfillment(bidOrders[2]), + newOrderFulfillment(askOrders[3]), + newOrderFulfillment(bidOrders[2]), }, - expSettlement: &Settlement{PartialOrderLeft: bidOrder(112, 1, "buyer112")}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - origAskOFs := copyOrderFulfillments(tc.askOFs) - origBidOFs := copyOrderFulfillments(tc.bidOFs) - - var err error + var actual []*orderFulfillment testFunc := func() { - err = splitPartial(tc.askOFs, tc.bidOFs, tc.settlement) - } - require.NotPanics(t, testFunc, "splitPartial") - assertions.AssertErrorValue(t, err, tc.expErr, "splitPartial error") - if len(tc.expErr) > 0 { - return - } - if !assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after splitPartial") { - t.Logf("Original: %s", orderFulfillmentsString(origAskOFs)) - } - if !assertEqualOrderFulfillmentSlices(t, tc.expBidOfs, tc.bidOFs, "bidOFs after splitPartial") { - t.Logf("Original: %s", orderFulfillmentsString(origBidOFs)) + actual = newOrderFulfillments(tc.orders) } - assert.Equalf(t, tc.expSettlement, tc.settlement, "settlement after splitPartial") + require.NotPanics(t, testFunc, "newOrderFulfillments") + assertEqualOrderFulfillmentSlices(t, tc.expected, actual, "newOrderFulfillments result") }) } } -func TestSplitOrderFulfillments(t *testing.T) { - acoin := func(amount int64) sdk.Coin { - return sdk.NewInt64Coin("acorn", amount) +func TestOrderFulfillment_AssetCoin(t *testing.T) { + coin := func(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} } - pcoin := func(amount int64) sdk.Coin { - return sdk.NewInt64Coin("prune", amount) + bigCoin := func(amount, denom string) sdk.Coin { + amt := newInt(t, amount) + return sdk.Coin{Denom: denom, Amount: amt} } - askOrder := func(orderID uint64, assetsAmt int64, allowPartial bool) *Order { + askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { return NewOrder(orderID).WithAsk(&AskOrder{ - MarketId: 123, - Seller: "sEllEr", - Assets: acoin(assetsAmt), - Price: pcoin(assetsAmt), - AllowPartial: allowPartial, + Assets: assets, + Price: price, }) } - bidOrder := func(orderID uint64, assetsAmt int64, allowPartial bool) *Order { + bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { return NewOrder(orderID).WithBid(&BidOrder{ - MarketId: 123, - Buyer: "bUyEr", - Assets: acoin(assetsAmt), - Price: pcoin(assetsAmt), - AllowPartial: allowPartial, + Assets: assets, + Price: price, }) } - newOF := func(order *Order, assetsFilledAmt int64) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - AssetsFilledAmt: sdkmath.NewInt(assetsFilledAmt), - AssetsUnfilledAmt: order.GetAssets().Amount.SubRaw(assetsFilledAmt), - PriceAppliedAmt: sdkmath.NewInt(assetsFilledAmt), - PriceLeftAmt: order.GetPrice().Amount.SubRaw(assetsFilledAmt), - } - // int(x).Sub(x) results in an object that is not .Equal to ZeroInt(). - // The Split function sets this to ZeroInt(). - if rv.AssetsUnfilledAmt.IsZero() { - rv.AssetsUnfilledAmt = sdkmath.ZeroInt() - } - return rv - } tests := []struct { - name string - fulfillments []*OrderFulfillment - settlement *Settlement - expFulfillments []*OrderFulfillment - expSettlement *Settlement - expErr string + name string + receiver orderFulfillment + amt sdkmath.Int + expected sdk.Coin + expPanic string }{ { - name: "one order, ask: nothing filled", - fulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, false), 0)}, - settlement: &Settlement{}, - expErr: "ask order 8 (at index 0) has no assets filled", - }, - { - name: "one order, bid: nothing filled", - fulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, false), 0)}, - settlement: &Settlement{}, - expErr: "bid order 8 (at index 0) has no assets filled", + name: "nil order", + receiver: orderFulfillment{Order: nil}, + amt: sdkmath.NewInt(0), + expPanic: "runtime error: invalid memory address or nil pointer dereference", }, { - name: "one order, ask: partially filled", - fulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, true), 13)}, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{newOF(askOrder(8, 13, true), 13)}, - expSettlement: &Settlement{PartialOrderLeft: askOrder(8, 40, true)}, + name: "nil inside order", + receiver: orderFulfillment{Order: NewOrder(1)}, + amt: sdkmath.NewInt(0), + expPanic: nilSubTypeErr(1), }, { - name: "one order, bid: partially filled", - fulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, true), 13)}, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{newOF(bidOrder(8, 13, true), 13)}, - expSettlement: &Settlement{PartialOrderLeft: bidOrder(8, 40, true)}, + name: "unknown inside order", + receiver: orderFulfillment{Order: newUnknownOrder(2)}, + amt: sdkmath.NewInt(0), + expPanic: unknownSubTypeErr(2), }, { - name: "one order, ask: partially filled, already have a partially filled", - fulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, true), 13)}, - settlement: &Settlement{PartialOrderLeft: bidOrder(951, 357, true)}, - expErr: "bid order 951 and ask order 8 cannot both be partially filled", + name: "ask order", + receiver: orderFulfillment{Order: askOrder(3, coin(4, "apple"), coin(5, "plum"))}, + amt: sdkmath.NewInt(6), + expected: coin(6, "apple"), }, { - name: "one order, bid: partially filled, already have a partially filled", - fulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, true), 13)}, - settlement: &Settlement{PartialOrderLeft: askOrder(951, 357, true)}, - expErr: "ask order 951 and bid order 8 cannot both be partially filled", + name: "ask order with negative assets", + receiver: orderFulfillment{Order: askOrder(7, coin(-8, "apple"), coin(9, "plum"))}, + amt: sdkmath.NewInt(10), + expected: coin(10, "apple"), }, { - name: "one order, ask: partially filled, split not allowed", - fulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, false), 13)}, - settlement: &Settlement{}, - expErr: "cannot split ask order 8 having assets \"53acorn\" at \"13acorn\": order does not allow partial fulfillment", + name: "ask order, negative amt", + receiver: orderFulfillment{Order: askOrder(11, coin(12, "apple"), coin(13, "plum"))}, + amt: sdkmath.NewInt(-14), + expected: coin(-14, "apple"), }, { - name: "one order, bid: partially filled, split not allowed", - fulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, false), 13)}, - settlement: &Settlement{}, - expErr: "cannot split bid order 8 having assets \"53acorn\" at \"13acorn\": order does not allow partial fulfillment", + name: "ask order with negative assets, negative amt", + receiver: orderFulfillment{Order: askOrder(15, coin(-16, "apple"), coin(17, "plum"))}, + amt: sdkmath.NewInt(-18), + expected: coin(-18, "apple"), }, { - name: "one order, ask: fully filled", - fulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, false), 53)}, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, false), 53)}, - expSettlement: &Settlement{}, + name: "ask order, big amt", + receiver: orderFulfillment{Order: askOrder(19, coin(20, "apple"), coin(21, "plum"))}, + amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), + expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "apple"), }, { - name: "one order, bid: fully filled", - fulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, false), 53)}, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, false), 53)}, - expSettlement: &Settlement{}, + name: "bid order", + receiver: orderFulfillment{Order: bidOrder(3, coin(4, "apple"), coin(5, "plum"))}, + amt: sdkmath.NewInt(6), + expected: coin(6, "apple"), }, { - name: "one order, ask: fully filled, already have a partially filled", - fulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, false), 53)}, - settlement: &Settlement{PartialOrderLeft: bidOrder(951, 357, true)}, - expFulfillments: []*OrderFulfillment{newOF(askOrder(8, 53, false), 53)}, - expSettlement: &Settlement{PartialOrderLeft: bidOrder(951, 357, true)}, + name: "bid order with negative assets", + receiver: orderFulfillment{Order: bidOrder(7, coin(-8, "apple"), coin(9, "plum"))}, + amt: sdkmath.NewInt(10), + expected: coin(10, "apple"), }, { - name: "one order, bid: fully filled, already have a partially filled", - fulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, false), 53)}, - settlement: &Settlement{PartialOrderLeft: askOrder(951, 357, true)}, - expFulfillments: []*OrderFulfillment{newOF(bidOrder(8, 53, false), 53)}, - expSettlement: &Settlement{PartialOrderLeft: askOrder(951, 357, true)}, + name: "bid order, negative amt", + receiver: orderFulfillment{Order: bidOrder(11, coin(12, "apple"), coin(13, "plum"))}, + amt: sdkmath.NewInt(-14), + expected: coin(-14, "apple"), }, { - name: "three orders, ask: second partially filled", - fulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, true), 16), - newOF(askOrder(10, 200, false), 0), - }, - settlement: &Settlement{}, - expErr: "ask order 9 (at index 1) is not filled in full and is not the last ask order provided", + name: "bid order with negative assets, negative amt", + receiver: orderFulfillment{Order: bidOrder(15, coin(-16, "apple"), coin(17, "plum"))}, + amt: sdkmath.NewInt(-18), + expected: coin(-18, "apple"), }, { - name: "three orders, bid: second partially filled", - fulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, true), 16), - newOF(bidOrder(10, 200, false), 0), - }, - settlement: &Settlement{}, - expErr: "bid order 9 (at index 1) is not filled in full and is not the last bid order provided", + name: "bid order, big amt", + receiver: orderFulfillment{Order: bidOrder(19, coin(20, "apple"), coin(21, "plum"))}, + amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), + expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "apple"), }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual sdk.Coin + testFunc := func() { + actual = tc.receiver.AssetCoin(tc.amt) + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "AssetCoin(%s)", tc.amt) + assert.Equal(t, tc.expected.String(), actual.String(), "AssetCoin(%s) result", tc.amt) + }) + } +} + +func TestOrderFulfillment_PriceCoin(t *testing.T) { + coin := func(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} + } + bigCoin := func(amount, denom string) sdk.Coin { + amt := newInt(t, amount) + return sdk.Coin{Denom: denom, Amount: amt} + } + askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { + return NewOrder(orderID).WithAsk(&AskOrder{ + Assets: assets, + Price: price, + }) + } + bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + Assets: assets, + Price: price, + }) + } + + tests := []struct { + name string + receiver orderFulfillment + amt sdkmath.Int + expected sdk.Coin + expPanic string + }{ { - name: "three orders, ask: last not touched", - fulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, false), 17), - newOF(askOrder(10, 200, false), 0), - }, - settlement: &Settlement{}, - expErr: "ask order 10 (at index 2) has no assets filled", + name: "nil order", + receiver: orderFulfillment{Order: nil}, + amt: sdkmath.NewInt(0), + expPanic: "runtime error: invalid memory address or nil pointer dereference", }, { - name: "three orders, bid: last not touched", - fulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, false), 17), - newOF(bidOrder(10, 200, true), 0), - }, - settlement: &Settlement{}, - expErr: "bid order 10 (at index 2) has no assets filled", + name: "nil inside order", + receiver: orderFulfillment{Order: NewOrder(1)}, + amt: sdkmath.NewInt(0), + expPanic: nilSubTypeErr(1), }, { - name: "three orders, ask: last partially filled", - fulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, false), 17), - newOF(askOrder(10, 200, true), 183), - }, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, false), 17), - newOF(askOrder(10, 183, true), 183), - }, - expSettlement: &Settlement{PartialOrderLeft: askOrder(10, 17, true)}, + name: "unknown inside order", + receiver: orderFulfillment{Order: newUnknownOrder(2)}, + amt: sdkmath.NewInt(0), + expPanic: unknownSubTypeErr(2), }, { - name: "three orders, bid: last partially filled", - fulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, false), 17), - newOF(bidOrder(10, 200, true), 183), - }, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, false), 17), - newOF(bidOrder(10, 183, true), 183), - }, - expSettlement: &Settlement{PartialOrderLeft: bidOrder(10, 17, true)}, + name: "ask order", + receiver: orderFulfillment{Order: askOrder(3, coin(4, "apple"), coin(5, "plum"))}, + amt: sdkmath.NewInt(6), + expected: coin(6, "plum"), }, { - name: "three orders, ask: last partially filled, split not allowed", - fulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, true), 53), - newOF(askOrder(9, 17, true), 17), - newOF(askOrder(10, 200, false), 183), - }, - settlement: &Settlement{}, - expErr: "cannot split ask order 10 having assets \"200acorn\" at \"183acorn\": order does not allow partial fulfillment", + name: "ask order with negative assets", + receiver: orderFulfillment{Order: askOrder(7, coin(-8, "apple"), coin(9, "plum"))}, + amt: sdkmath.NewInt(10), + expected: coin(10, "plum"), }, { - name: "three orders, bid: last partially filled, split not allowed", - fulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, true), 53), - newOF(bidOrder(9, 17, true), 17), - newOF(bidOrder(10, 200, false), 183), - }, - settlement: &Settlement{}, - expErr: "cannot split bid order 10 having assets \"200acorn\" at \"183acorn\": order does not allow partial fulfillment", + name: "ask order, negative amt", + receiver: orderFulfillment{Order: askOrder(11, coin(12, "apple"), coin(13, "plum"))}, + amt: sdkmath.NewInt(-14), + expected: coin(-14, "plum"), }, { - name: "three orders, ask: last partially filled, already have a partially filled", - fulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, true), 53), - newOF(askOrder(9, 17, true), 17), - newOF(askOrder(10, 200, false), 183), - }, - settlement: &Settlement{PartialOrderLeft: bidOrder(857, 43, true)}, - expErr: "bid order 857 and ask order 10 cannot both be partially filled", + name: "ask order with negative assets, negative amt", + receiver: orderFulfillment{Order: askOrder(15, coin(-16, "apple"), coin(17, "plum"))}, + amt: sdkmath.NewInt(-18), + expected: coin(-18, "plum"), }, { - name: "three orders, bid: last partially filled, already have a partially filled", - fulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, true), 53), - newOF(bidOrder(9, 17, true), 17), - newOF(bidOrder(10, 200, false), 183), - }, - settlement: &Settlement{PartialOrderLeft: askOrder(857, 43, true)}, - expErr: "ask order 857 and bid order 10 cannot both be partially filled", + name: "ask order, big amt", + receiver: orderFulfillment{Order: askOrder(19, coin(20, "apple"), coin(21, "plum"))}, + amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), + expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "plum"), }, { - name: "three orders, ask: fully filled", - fulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, false), 17), - newOF(askOrder(10, 200, false), 200), - }, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, false), 17), - newOF(askOrder(10, 200, false), 200), - }, - expSettlement: &Settlement{}, + name: "bid order", + receiver: orderFulfillment{Order: bidOrder(3, coin(4, "apple"), coin(5, "plum"))}, + amt: sdkmath.NewInt(6), + expected: coin(6, "plum"), }, { - name: "three orders, bid: fully filled", - fulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, false), 17), - newOF(bidOrder(10, 200, false), 200), - }, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, false), 17), - newOF(bidOrder(10, 200, false), 200), - }, - expSettlement: &Settlement{}, + name: "bid order with negative assets", + receiver: orderFulfillment{Order: bidOrder(7, coin(-8, "apple"), coin(9, "plum"))}, + amt: sdkmath.NewInt(10), + expected: coin(10, "plum"), }, { - name: "three orders, ask: fully filled, already have a partially filled", - fulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, false), 17), - newOF(askOrder(10, 200, false), 200), - }, - settlement: &Settlement{}, - expFulfillments: []*OrderFulfillment{ - newOF(askOrder(8, 53, false), 53), - newOF(askOrder(9, 17, false), 17), - newOF(askOrder(10, 200, false), 200), - }, - expSettlement: &Settlement{}, + name: "bid order, negative amt", + receiver: orderFulfillment{Order: bidOrder(11, coin(12, "apple"), coin(13, "plum"))}, + amt: sdkmath.NewInt(-14), + expected: coin(-14, "plum"), }, { - name: "three orders, bid: fully filled, already have a partially filled", - fulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, false), 17), - newOF(bidOrder(10, 200, false), 200), - }, - settlement: &Settlement{PartialOrderLeft: askOrder(857, 43, true)}, - expFulfillments: []*OrderFulfillment{ - newOF(bidOrder(8, 53, false), 53), - newOF(bidOrder(9, 17, false), 17), - newOF(bidOrder(10, 200, false), 200), - }, - expSettlement: &Settlement{PartialOrderLeft: askOrder(857, 43, true)}, + name: "bid order with negative assets, negative amt", + receiver: orderFulfillment{Order: bidOrder(15, coin(-16, "apple"), coin(17, "plum"))}, + amt: sdkmath.NewInt(-18), + expected: coin(-18, "plum"), }, + { + name: "bid order, big amt", + receiver: orderFulfillment{Order: bidOrder(19, coin(20, "apple"), coin(21, "plum"))}, + amt: newInt(t, "123,000,000,000,000,000,000,000,000,000,000,321"), + expected: bigCoin("123,000,000,000,000,000,000,000,000,000,000,321", "plum"), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual sdk.Coin + testFunc := func() { + actual = tc.receiver.PriceCoin(tc.amt) + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "PriceCoin(%s)", tc.amt) + assert.Equal(t, tc.expected.String(), actual.String(), "PriceCoin(%s) result", tc.amt) + }) + } +} + +func TestOrderFulfillment_GetAssetsFilled(t *testing.T) { + coin := func(amt int64) sdk.Coin { + return sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(amt)} + } + askOrder := NewOrder(444).WithAsk(&AskOrder{Assets: coin(5555)}) + bidOrder := NewOrder(666).WithBid(&BidOrder{Assets: coin(7777)}) + + newOF := func(order *Order, amt int64) orderFulfillment { + return orderFulfillment{ + Order: order, + AssetsFilledAmt: sdkmath.NewInt(amt), + } + } + + tests := []struct { + name string + f orderFulfillment + exp sdk.Coin + }{ + {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, + {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, + {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, + {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, + {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, + {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual sdk.Coin + testFunc := func() { + actual = tc.f.GetAssetsFilled() + } + require.NotPanics(t, testFunc, "GetAssetsFilled()") + assert.Equal(t, tc.exp.String(), actual.String(), "GetAssetsFilled() result") + }) + } +} + +func TestOrderFulfillment_GetAssetsUnfilled(t *testing.T) { + coin := func(amt int64) sdk.Coin { + return sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(amt)} + } + askOrder := NewOrder(444).WithAsk(&AskOrder{Assets: coin(5555)}) + bidOrder := NewOrder(666).WithBid(&BidOrder{Assets: coin(7777)}) + + newOF := func(order *Order, amt int64) orderFulfillment { + return orderFulfillment{ + Order: order, + AssetsUnfilledAmt: sdkmath.NewInt(amt), + } + } + + tests := []struct { + name string + f orderFulfillment + exp sdk.Coin + }{ + {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, + {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, + {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, + {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, + {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, + {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual sdk.Coin + testFunc := func() { + actual = tc.f.GetAssetsUnfilled() + } + require.NotPanics(t, testFunc, "GetAssetsUnfilled()") + assert.Equal(t, tc.exp.String(), actual.String(), "GetAssetsUnfilled() result") + }) + } +} + +func TestOrderFulfillment_GetPriceApplied(t *testing.T) { + coin := func(amt int64) sdk.Coin { + return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} + } + askOrder := NewOrder(444).WithAsk(&AskOrder{Price: coin(5555)}) + bidOrder := NewOrder(666).WithBid(&BidOrder{Price: coin(7777)}) + + newOF := func(order *Order, amt int64) orderFulfillment { + return orderFulfillment{ + Order: order, + PriceAppliedAmt: sdkmath.NewInt(amt), + } + } + + tests := []struct { + name string + f orderFulfillment + exp sdk.Coin + }{ + {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, + {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, + {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, + {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, + {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, + {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual sdk.Coin + testFunc := func() { + actual = tc.f.GetPriceApplied() + } + require.NotPanics(t, testFunc, "GetPriceApplied()") + assert.Equal(t, tc.exp.String(), actual.String(), "GetPriceApplied() result") + }) + } +} + +func TestOrderFulfillment_GetPriceLeft(t *testing.T) { + coin := func(amt int64) sdk.Coin { + return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} + } + askOrder := NewOrder(444).WithAsk(&AskOrder{Price: coin(5555)}) + bidOrder := NewOrder(666).WithBid(&BidOrder{Price: coin(7777)}) + + newOF := func(order *Order, amt int64) orderFulfillment { + return orderFulfillment{ + Order: order, + PriceLeftAmt: sdkmath.NewInt(amt), + } + } + + tests := []struct { + name string + f orderFulfillment + exp sdk.Coin + }{ + {name: "positive ask", f: newOF(askOrder, 2), exp: coin(2)}, + {name: "zero ask", f: newOF(askOrder, 0), exp: coin(0)}, + {name: "negative ask", f: newOF(askOrder, -3), exp: coin(-3)}, + {name: "positive bid", f: newOF(bidOrder, 2), exp: coin(2)}, + {name: "zero bid", f: newOF(bidOrder, 0), exp: coin(0)}, + {name: "negative bid", f: newOF(bidOrder, -3), exp: coin(-3)}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var err error + var actual sdk.Coin testFunc := func() { - err = splitOrderFulfillments(tc.fulfillments, tc.settlement) + actual = tc.f.GetPriceLeft() } - require.NotPanics(t, testFunc, "splitOrderFulfillments") - assertions.AssertErrorValue(t, err, tc.expErr, "splitOrderFulfillments error") - if len(tc.expErr) > 0 { - return + require.NotPanics(t, testFunc, "GetPriceLeft()") + assert.Equal(t, tc.exp.String(), actual.String(), "GetPriceLeft() result") + }) + } +} + +func TestOrderFulfillment_GetOrderID(t *testing.T) { + newOF := func(orderID uint64) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(orderID), + } + } + + tests := []struct { + name string + f orderFulfillment + exp uint64 + }{ + {name: "zero", f: newOF(0), exp: 0}, + {name: "one", f: newOF(1), exp: 1}, + {name: "five", f: newOF(5), exp: 5}, + {name: "max uint32+1", f: newOF(4_294_967_296), exp: 4_294_967_296}, + {name: "max uint64", f: newOF(18_446_744_073_709_551_615), exp: 18_446_744_073_709_551_615}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual uint64 + testFunc := func() { + actual = tc.f.GetOrderID() } - assertEqualOrderFulfillmentSlices(t, tc.expFulfillments, tc.fulfillments, "fulfillments after splitOrderFulfillments") - assert.Equal(t, tc.expSettlement, tc.settlement, "settlement after splitOrderFulfillments") + require.NotPanics(t, testFunc, "GetOrderID()") + assert.Equal(t, tc.exp, actual, "GetOrderID() result") }) } } -func TestAllocatePrice(t *testing.T) { - askOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - MarketId: 123, - Seller: fmt.Sprintf("seller%d", orderID), - Assets: sdk.NewInt64Coin("apple", assetsAmt), - Price: sdk.NewInt64Coin("peach", priceAmt), +func TestOrderFulfillment_IsAskOrder(t *testing.T) { + tests := []struct { + name string + f orderFulfillment + exp bool + }{ + {name: "ask", f: orderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: true}, + {name: "bid", f: orderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: false}, + {name: "nil", f: orderFulfillment{Order: NewOrder(888)}, exp: false}, + {name: "unknown", f: orderFulfillment{Order: newUnknownOrder(7)}, exp: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual bool + testFunc := func() { + actual = tc.f.IsAskOrder() + } + require.NotPanics(t, testFunc, "IsAskOrder()") + assert.Equal(t, tc.exp, actual, "IsAskOrder() result") }) } - bidOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - MarketId: 123, - Buyer: fmt.Sprintf("buyer%d", orderID), - Assets: sdk.NewInt64Coin("apple", assetsAmt), - Price: sdk.NewInt64Coin("peach", priceAmt), +} + +func TestOrderFulfillment_IsBidOrder(t *testing.T) { + tests := []struct { + name string + f orderFulfillment + exp bool + }{ + {name: "ask", f: orderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: false}, + {name: "bid", f: orderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: true}, + {name: "nil", f: orderFulfillment{Order: NewOrder(888)}, exp: false}, + {name: "unknown", f: orderFulfillment{Order: newUnknownOrder(9)}, exp: false}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual bool + testFunc := func() { + actual = tc.f.IsBidOrder() + } + require.NotPanics(t, testFunc, "IsBidOrder()") + assert.Equal(t, tc.exp, actual, "IsBidOrder() result") }) } - newOF := func(order *Order, dists ...*Distribution) *OrderFulfillment { - rv := NewOrderFulfillment(order) - rv.AssetsFilledAmt, rv.AssetsUnfilledAmt = rv.AssetsUnfilledAmt, rv.AssetsFilledAmt - if len(dists) > 0 { - rv.PriceDists = dists - for _, dist := range dists { - rv.PriceAppliedAmt = rv.PriceAppliedAmt.Add(dist.Amount) +} + +func TestOrderFulfillment_GetMarketID(t *testing.T) { + askOrder := func(marketID uint32) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithAsk(&AskOrder{MarketId: marketID}), + } + } + bidOrder := func(marketID uint32) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithBid(&BidOrder{MarketId: marketID}), + } + } + + tests := []struct { + name string + f orderFulfillment + exp uint32 + }{ + {name: "ask zero", f: askOrder(0), exp: 0}, + {name: "ask one", f: askOrder(1), exp: 1}, + {name: "ask five", f: askOrder(5), exp: 5}, + {name: "ask max uint16+1", f: askOrder(65_536), exp: 65_536}, + {name: "ask max uint32", f: askOrder(4_294_967_295), exp: 4_294_967_295}, + {name: "bid zero", f: bidOrder(0), exp: 0}, + {name: "bid one", f: bidOrder(1), exp: 1}, + {name: "bid five", f: bidOrder(5), exp: 5}, + {name: "bid max uint16+1", f: bidOrder(65_536), exp: 65_536}, + {name: "bid max uint32", f: bidOrder(4_294_967_295), exp: 4_294_967_295}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual uint32 + testFunc := func() { + actual = tc.f.GetMarketID() } - rv.PriceLeftAmt = rv.PriceLeftAmt.Sub(rv.PriceAppliedAmt) + require.NotPanics(t, testFunc, "GetMarketID()") + assert.Equal(t, tc.exp, actual, "GetMarketID() result") + }) + } +} + +func TestOrderFulfillment_GetOwner(t *testing.T) { + askOrder := func(seller string) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithAsk(&AskOrder{Seller: seller}), } - return rv } - dist := func(address string, amount int64) *Distribution { - return &Distribution{Address: address, Amount: sdkmath.NewInt(amount)} + bidOrder := func(buyer string) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithBid(&BidOrder{Buyer: buyer}), + } } + owner := sdk.AccAddress("owner_______________").String() tests := []struct { - name string - askOFs []*OrderFulfillment - bidOFs []*OrderFulfillment - expAskOFs []*OrderFulfillment - expBidOFs []*OrderFulfillment - expErr string + name string + f orderFulfillment + exp string }{ - { - name: "total ask price greater than total bid", - askOFs: []*OrderFulfillment{ - newOF(askOrder(3, 10, 20)), - newOF(askOrder(4, 10, 20)), - newOF(askOrder(5, 10, 20)), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(6, 10, 20)), - newOF(bidOrder(7, 10, 19)), - newOF(bidOrder(8, 10, 20)), - }, - expErr: "total ask price \"60peach\" is greater than total bid price \"59peach\"", - }, - { - name: "one ask, one bid: same price", - askOFs: []*OrderFulfillment{newOF(askOrder(3, 10, 60))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(6, 10, 60))}, - expAskOFs: []*OrderFulfillment{newOF(askOrder(3, 10, 60), dist("buyer6", 60))}, - expBidOFs: []*OrderFulfillment{newOF(bidOrder(6, 10, 60), dist("seller3", 60))}, - }, - { - name: "one ask, one bid: bid more", - askOFs: []*OrderFulfillment{newOF(askOrder(3, 10, 60))}, - bidOFs: []*OrderFulfillment{newOF(bidOrder(6, 10, 65))}, - expAskOFs: []*OrderFulfillment{newOF(askOrder(3, 10, 60), dist("buyer6", 60), dist("buyer6", 5))}, - expBidOFs: []*OrderFulfillment{newOF(bidOrder(6, 10, 65), dist("seller3", 60), dist("seller3", 5))}, - }, - { - name: "two asks, two bids: same total price, diff ask prices", - askOFs: []*OrderFulfillment{ - newOF(askOrder(3, 10, 21)), - newOF(askOrder(4, 10, 19)), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(6, 10, 20)), - newOF(bidOrder(7, 10, 20)), - }, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(3, 10, 21), dist("buyer6", 20), dist("buyer7", 1)), - newOF(askOrder(4, 10, 19), dist("buyer7", 19)), - }, - expBidOFs: []*OrderFulfillment{ - newOF(bidOrder(6, 10, 20), dist("seller3", 20)), - newOF(bidOrder(7, 10, 20), dist("seller3", 1), dist("seller4", 19)), - }, - }, - { - name: "three asks, three bids: same total price", - askOFs: []*OrderFulfillment{ - newOF(askOrder(3, 10, 25)), - newOF(askOrder(4, 10, 20)), - newOF(askOrder(5, 10, 15)), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(6, 10, 18)), - newOF(bidOrder(7, 10, 30)), - newOF(bidOrder(8, 10, 12)), - }, - expAskOFs: []*OrderFulfillment{ - newOF(askOrder(3, 10, 25), dist("buyer6", 18), dist("buyer7", 7)), - newOF(askOrder(4, 10, 20), dist("buyer7", 20)), - newOF(askOrder(5, 10, 15), dist("buyer7", 3), dist("buyer8", 12)), - }, - expBidOFs: []*OrderFulfillment{ - newOF(bidOrder(6, 10, 18), dist("seller3", 18)), - newOF(bidOrder(7, 10, 30), dist("seller3", 7), dist("seller4", 20), dist("seller5", 3)), - newOF(bidOrder(8, 10, 12), dist("seller5", 12)), - }, - }, - { - name: "three asks, three bids: bids more", - askOFs: []*OrderFulfillment{ - newOF(askOrder(3, 1, 10)), - newOF(askOrder(4, 7, 25)), - newOF(askOrder(5, 22, 30)), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(6, 10, 20)), - newOF(bidOrder(7, 10, 27)), - newOF(bidOrder(8, 10, 30)), - }, - // assets total = 30 - // ask price total = 65 - // bid price total = 77 - // leftover = 12 - expAskOFs: []*OrderFulfillment{ - // 12 * 1 / 30 = 0.4 => 0, then 1 - newOF(askOrder(3, 1, 10), dist("buyer6", 10), - dist("buyer8", 1)), - // 12 * 7 / 30 = 2.8 => 2, then because there'll only be 1 left, 1 - newOF(askOrder(4, 7, 25), dist("buyer6", 10), dist("buyer7", 15), - dist("buyer8", 2), dist("buyer8", 1)), - // 12 * 22 / 30 = 8.8 => 8, then nothing because leftovers run out before getting back to it. - newOF(askOrder(5, 22, 30), dist("buyer7", 12), dist("buyer8", 18), - dist("buyer8", 8)), - }, - expBidOFs: []*OrderFulfillment{ - newOF(bidOrder(6, 10, 20), dist("seller3", 10), dist("seller4", 10)), - newOF(bidOrder(7, 10, 27), dist("seller4", 15), dist("seller5", 12)), - newOF(bidOrder(8, 10, 30), dist("seller5", 18), dist("seller4", 2), - dist("seller5", 8), dist("seller3", 1), dist("seller4", 1)), - }, - }, + {name: "ask empty", f: askOrder(""), exp: ""}, + {name: "ask not a bech32", f: askOrder("owner"), exp: "owner"}, + {name: "ask beche32", f: askOrder(owner), exp: owner}, + {name: "bid empty", f: bidOrder(""), exp: ""}, + {name: "bid not a bech32", f: bidOrder("owner"), exp: "owner"}, + {name: "bid beche32", f: bidOrder(owner), exp: owner}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual string + testFunc := func() { + actual = tc.f.GetOwner() + } + require.NotPanics(t, testFunc, "GetOwner()") + assert.Equal(t, tc.exp, actual, "GetOwner() result") + }) + } +} + +func TestOrderFulfillment_GetAssets(t *testing.T) { + coin := func(amt int64) sdk.Coin { + return sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(amt)} + } + askOrder := func(amt int64) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithAsk(&AskOrder{Assets: coin(amt)}), + } + } + bidOrder := func(amt int64) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithBid(&BidOrder{Assets: coin(amt)}), + } + } + + tests := []struct { + name string + f orderFulfillment + exp sdk.Coin + }{ + {name: "ask positive", f: askOrder(123), exp: coin(123)}, + {name: "ask zero", f: askOrder(0), exp: coin(0)}, + {name: "ask negative", f: askOrder(-9), exp: coin(-9)}, + {name: "bid positive", f: bidOrder(345), exp: coin(345)}, + {name: "bid zero", f: bidOrder(0), exp: coin(0)}, + {name: "bid negative", f: bidOrder(-8), exp: coin(-8)}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var err error + var actual sdk.Coin testFunc := func() { - err = allocatePrice(tc.askOFs, tc.bidOFs) - } - require.NotPanics(t, testFunc, "allocatePrice") - assertions.AssertErrorValue(t, err, tc.expErr, "allocatePrice error") - if len(tc.expErr) > 0 { - return + actual = tc.f.GetAssets() } - assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after allocatePrice") - assertEqualOrderFulfillmentSlices(t, tc.expBidOFs, tc.bidOFs, "bidOFs after allocatePrice") + require.NotPanics(t, testFunc, "GetAssets()") + assert.Equal(t, tc.exp.String(), actual.String(), "GetAssets() result") }) } } -func TestSetFeesToPay(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} +func TestOrderFulfillment_GetPrice(t *testing.T) { + coin := func(amt int64) sdk.Coin { + return sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(amt)} } - askOF := func(orderID uint64, priceAppliedAmt int64, fees ...sdk.Coin) *OrderFulfillment { - askOrder := &AskOrder{Price: coin(50, "plum")} - if len(fees) > 1 { - t.Fatalf("cannot provide more than one fee to askOF(%d, %d, %q)", - orderID, priceAppliedAmt, fees) - } - if len(fees) > 0 { - askOrder.SellerSettlementFlatFee = &fees[0] - } - return &OrderFulfillment{ - Order: NewOrder(orderID).WithAsk(askOrder), - PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), + askOrder := func(amt int64) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithAsk(&AskOrder{Price: coin(amt)}), } } - bidOF := func(orderID uint64, priceAppliedAmt int64, fees ...sdk.Coin) *OrderFulfillment { - bidOrder := &BidOrder{Price: coin(50, "plum")} - if len(fees) > 0 { - bidOrder.BuyerSettlementFees = fees + bidOrder := func(amt int64) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithBid(&BidOrder{Price: coin(amt)}), } - return &OrderFulfillment{ - Order: NewOrder(orderID).WithBid(bidOrder), - PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), + } + + tests := []struct { + name string + f orderFulfillment + exp sdk.Coin + }{ + {name: "ask positive", f: askOrder(123), exp: coin(123)}, + {name: "ask zero", f: askOrder(0), exp: coin(0)}, + {name: "ask negative", f: askOrder(-9), exp: coin(-9)}, + {name: "bid positive", f: bidOrder(345), exp: coin(345)}, + {name: "bid zero", f: bidOrder(0), exp: coin(0)}, + {name: "bid negative", f: bidOrder(-8), exp: coin(-8)}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual sdk.Coin + testFunc := func() { + actual = tc.f.GetPrice() + } + require.NotPanics(t, testFunc, "GetPrice()") + assert.Equal(t, tc.exp.String(), actual.String(), "GetPrice() result") + }) + } +} + +func TestOrderFulfillment_GetSettlementFees(t *testing.T) { + coin := func(amt int64) *sdk.Coin { + return &sdk.Coin{Denom: "fees", Amount: sdkmath.NewInt(amt)} + } + askOrder := func(coin *sdk.Coin) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithAsk(&AskOrder{SellerSettlementFlatFee: coin}), } } - expOF := func(f *OrderFulfillment, feesToPay ...sdk.Coin) *OrderFulfillment { - if len(feesToPay) > 0 { - f.FeesToPay = sdk.NewCoins(feesToPay...) + bidOrder := func(coins sdk.Coins) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithBid(&BidOrder{BuyerSettlementFees: coins}), } - return f } tests := []struct { - name string - askOFs []*OrderFulfillment - bidOFs []*OrderFulfillment - ratio *FeeRatio - expAskOFs []*OrderFulfillment - expBidOFs []*OrderFulfillment - expErr string + name string + f orderFulfillment + exp sdk.Coins }{ + {name: "ask nil", f: askOrder(nil), exp: nil}, + {name: "ask zero", f: askOrder(coin(0)), exp: sdk.Coins{*coin(0)}}, + {name: "ask positive", f: askOrder(coin(3)), exp: sdk.Coins{*coin(3)}}, + {name: "bid nil", f: bidOrder(nil), exp: nil}, + {name: "bid empty", f: bidOrder(sdk.Coins{}), exp: sdk.Coins{}}, + {name: "bid positive", f: bidOrder(sdk.Coins{*coin(3)}), exp: sdk.Coins{*coin(3)}}, + {name: "bid zero", f: bidOrder(sdk.Coins{*coin(0)}), exp: sdk.Coins{*coin(0)}}, + {name: "bid negative", f: bidOrder(sdk.Coins{*coin(-2)}), exp: sdk.Coins{*coin(-2)}}, { - name: "cannot apply ratio", - askOFs: []*OrderFulfillment{ - askOF(7777, 55, coin(20, "grape")), - askOF(5555, 71), - askOF(6666, 100), - }, - bidOFs: []*OrderFulfillment{ - bidOF(1111, 100), - bidOF(2222, 200, coin(20, "grape")), - bidOF(3333, 300), - }, - ratio: &FeeRatio{Price: coin(30, "peach"), Fee: coin(1, "fig")}, - expAskOFs: []*OrderFulfillment{ - expOF(askOF(7777, 55, coin(20, "grape"))), - expOF(askOF(5555, 71)), - expOF(askOF(6666, 100)), - }, - expBidOFs: []*OrderFulfillment{ - expOF(bidOF(1111, 100)), - expOF(bidOF(2222, 200, coin(20, "grape")), coin(20, "grape")), - expOF(bidOF(3333, 300)), - }, - expErr: joinErrs( - "failed calculate ratio fee for ask order 7777: cannot apply ratio 30peach:1fig to price 55plum: incorrect price denom", - "failed calculate ratio fee for ask order 5555: cannot apply ratio 30peach:1fig to price 71plum: incorrect price denom", - "failed calculate ratio fee for ask order 6666: cannot apply ratio 30peach:1fig to price 100plum: incorrect price denom", - ), - }, - { - name: "no ratio", - askOFs: []*OrderFulfillment{ - askOF(7777, 55, coin(20, "grape")), - askOF(5555, 71), - askOF(6666, 100), - }, - bidOFs: []*OrderFulfillment{ - bidOF(1111, 100), - bidOF(2222, 200, coin(20, "grape")), - bidOF(3333, 300), - }, - ratio: nil, - expAskOFs: []*OrderFulfillment{ - expOF(askOF(7777, 55, coin(20, "grape")), coin(20, "grape")), - expOF(askOF(5555, 71)), - expOF(askOF(6666, 100)), - }, - expBidOFs: []*OrderFulfillment{ - expOF(bidOF(1111, 100)), - expOF(bidOF(2222, 200, coin(20, "grape")), coin(20, "grape")), - expOF(bidOF(3333, 300)), - }, - }, - { - name: "with ratio", - askOFs: []*OrderFulfillment{ - askOF(7777, 55, coin(20, "grape")), - askOF(5555, 71), - askOF(6666, 100), - }, - bidOFs: []*OrderFulfillment{ - bidOF(1111, 100), - bidOF(2222, 200, coin(20, "grape")), - bidOF(3333, 300), - }, - ratio: &FeeRatio{Price: coin(30, "plum"), Fee: coin(1, "fig")}, - expAskOFs: []*OrderFulfillment{ - expOF(askOF(7777, 55, coin(20, "grape")), coin(2, "fig"), coin(20, "grape")), - expOF(askOF(5555, 71), coin(3, "fig")), - expOF(askOF(6666, 100), coin(4, "fig")), - }, - expBidOFs: []*OrderFulfillment{ - expOF(bidOF(1111, 100)), - expOF(bidOF(2222, 200, coin(20, "grape")), coin(20, "grape")), - expOF(bidOF(3333, 300)), - }, + name: "bid multiple", + f: bidOrder(sdk.Coins{*coin(987), sdk.NewInt64Coin("six", 6), sdk.Coin{Denom: "zeg", Amount: sdkmath.NewInt(-1)}}), + exp: sdk.Coins{*coin(987), sdk.NewInt64Coin("six", 6), sdk.Coin{Denom: "zeg", Amount: sdkmath.NewInt(-1)}}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var err error + var actual sdk.Coins testFunc := func() { - err = setFeesToPay(tc.askOFs, tc.bidOFs, tc.ratio) + actual = tc.f.GetSettlementFees() } - require.NotPanics(t, testFunc, "setFeesToPay") - assertions.AssertErrorValue(t, err, tc.expErr, "setFeesToPay error") - assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after setFeesToPay") - assertEqualOrderFulfillmentSlices(t, tc.expBidOFs, tc.bidOFs, "bidOFs after setFeesToPay") + require.NotPanics(t, testFunc, "GetSettlementFees()") + assert.Equal(t, tc.exp.String(), actual.String(), "GetSettlementFees() result") }) } } -func TestValidateFulfillments(t *testing.T) { - goodAskOF := func(orderID uint64) *OrderFulfillment { - return &OrderFulfillment{ - Order: NewOrder(orderID).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("peach", 123), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceAppliedAmt: sdkmath.NewInt(130), +func TestOrderFulfillment_PartialFillAllowed(t *testing.T) { + askOrder := func(allowPartial bool) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithAsk(&AskOrder{AllowPartial: allowPartial}), } } - badAskOF := func(orderID uint64) *OrderFulfillment { - rv := goodAskOF(orderID) - rv.AssetsFilledAmt = sdkmath.NewInt(49) - return rv - } - badAskErr := func(orderID uint64) string { - return badAskOF(orderID).Validate().Error() - } - goodBidOF := func(orderID uint64) *OrderFulfillment { - return &OrderFulfillment{ - Order: NewOrder(orderID).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("peach", 123), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceAppliedAmt: sdkmath.NewInt(123), + bidOrder := func(allowPartial bool) orderFulfillment { + return orderFulfillment{ + Order: NewOrder(999).WithBid(&BidOrder{AllowPartial: allowPartial}), } } - badBidOF := func(orderID uint64) *OrderFulfillment { - rv := goodBidOF(orderID) - rv.AssetsFilledAmt = sdkmath.NewInt(49) - return rv + + tests := []struct { + name string + f orderFulfillment + exp bool + }{ + {name: "ask true", f: askOrder(true), exp: true}, + {name: "ask false", f: askOrder(false), exp: false}, + {name: "bid true", f: bidOrder(true), exp: true}, + {name: "bid false", f: bidOrder(false), exp: false}, } - badBidErr := func(orderID uint64) string { - return badBidOF(orderID).Validate().Error() + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual bool + testFunc := func() { + actual = tc.f.PartialFillAllowed() + } + require.NotPanics(t, testFunc, "PartialFillAllowed()") + assert.Equal(t, tc.exp, actual, "PartialFillAllowed() result") + }) } +} +func TestOrderFulfillment_GetOrderType(t *testing.T) { tests := []struct { - name string - askOFs []*OrderFulfillment - bidOFs []*OrderFulfillment - expErr string + name string + f orderFulfillment + exp string }{ - { - name: "all good", - askOFs: []*OrderFulfillment{goodAskOF(10), goodAskOF(11), goodAskOF(12)}, - bidOFs: []*OrderFulfillment{goodBidOF(20), goodBidOF(21), goodBidOF(22)}, - expErr: "", - }, - { - name: "error in one ask", - askOFs: []*OrderFulfillment{goodAskOF(10), badAskOF(11), goodAskOF(12)}, - bidOFs: []*OrderFulfillment{goodBidOF(20), goodBidOF(21), goodBidOF(22)}, - expErr: badAskErr(11), - }, - { - name: "error in one bid", - askOFs: []*OrderFulfillment{goodAskOF(10), goodAskOF(11), goodAskOF(12)}, - bidOFs: []*OrderFulfillment{goodBidOF(20), badBidOF(21), goodBidOF(22)}, - expErr: badBidErr(21), - }, - { - name: "two errors in asks", - askOFs: []*OrderFulfillment{badAskOF(10), goodAskOF(11), badAskOF(12)}, - bidOFs: []*OrderFulfillment{goodBidOF(20), goodBidOF(21), goodBidOF(22)}, - expErr: joinErrs(badAskErr(10), badAskErr(12)), - }, - { - name: "two errors in bids", - askOFs: []*OrderFulfillment{goodAskOF(10), goodAskOF(11), goodAskOF(12)}, - bidOFs: []*OrderFulfillment{badBidOF(20), goodBidOF(21), badBidOF(22)}, - expErr: joinErrs(badBidErr(20), badBidErr(22)), - }, - { - name: "error in each", - askOFs: []*OrderFulfillment{goodAskOF(10), goodAskOF(11), badAskOF(12)}, - bidOFs: []*OrderFulfillment{goodBidOF(20), badBidOF(21), goodBidOF(22)}, - expErr: joinErrs(badAskErr(12), badBidErr(21)), - }, + {name: "ask", f: orderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: OrderTypeAsk}, + {name: "bid", f: orderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: OrderTypeBid}, + {name: "nil", f: orderFulfillment{Order: NewOrder(888)}, exp: ""}, + {name: "unknown", f: orderFulfillment{Order: newUnknownOrder(8)}, exp: "*exchange.unknownOrderType"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual string + testFunc := func() { + actual = tc.f.GetOrderType() + } + require.NotPanics(t, testFunc, "GetOrderType()") + assert.Equal(t, tc.exp, actual, "GetOrderType() result") + }) + } +} + +func TestOrderFulfillment_GetOrderTypeByte(t *testing.T) { + tests := []struct { + name string + f orderFulfillment + exp byte + }{ + {name: "ask", f: orderFulfillment{Order: NewOrder(444).WithAsk(&AskOrder{})}, exp: OrderTypeByteAsk}, + {name: "bid", f: orderFulfillment{Order: NewOrder(666).WithBid(&BidOrder{})}, exp: OrderTypeByteBid}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var err error + var actual byte testFunc := func() { - err = validateFulfillments(tc.askOFs, tc.bidOFs) + actual = tc.f.GetOrderTypeByte() } - require.NotPanics(t, testFunc, "validateFulfillments") - assertions.AssertErrorValue(t, err, tc.expErr, "validateFulfillments error") + require.NotPanics(t, testFunc, "GetOrderTypeByte()") + assert.Equal(t, tc.exp, actual, "GetOrderTypeByte() result") }) } } -func TestBuildTransfers(t *testing.T) { +func TestOrderFulfillment_GetHoldAmount(t *testing.T) { tests := []struct { - name string - askOFs []*OrderFulfillment - bidOFs []*OrderFulfillment - expSettlement *Settlement - expErr string + name string + f orderFulfillment }{ { - name: "ask with negative assets filled", - askOFs: []*OrderFulfillment{ - { - Order: NewOrder(18).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 15), - Price: sdk.NewInt64Coin("plum", 42), - }), - AssetsFilledAmt: sdkmath.NewInt(-1), - PriceAppliedAmt: sdkmath.NewInt(-1), - }, - }, - expErr: "ask order 18 cannot be filled with \"-1apple\" assets: amount not positive", - }, - { - name: "bid with negative assets filled", - bidOFs: []*OrderFulfillment{ - { - Order: NewOrder(12).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 15), - Price: sdk.NewInt64Coin("plum", 42), - }), - AssetsFilledAmt: sdkmath.NewInt(-1), - PriceAppliedAmt: sdkmath.NewInt(-1), - }, + name: "ask", + f: orderFulfillment{ + Order: NewOrder(111).WithAsk(&AskOrder{ + Assets: sdk.Coin{Denom: "asset", Amount: sdkmath.NewInt(55)}, + SellerSettlementFlatFee: &sdk.Coin{Denom: "fee", Amount: sdkmath.NewInt(3)}, + }), }, - expErr: "bid order 12 cannot be filled at price \"-1plum\": amount not positive", }, { - name: "ask with negative fees to pay", - askOFs: []*OrderFulfillment{ - { - Order: NewOrder(53).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 15), - Price: sdk.NewInt64Coin("plum", 42), - }), - AssetsFilledAmt: sdkmath.NewInt(15), - AssetDists: []*Distribution{{Address: "buyer1", Amount: sdkmath.NewInt(15)}}, - PriceAppliedAmt: sdkmath.NewInt(42), - PriceDists: []*Distribution{{Address: "seller1", Amount: sdkmath.NewInt(42)}}, - FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(-1)}}, - }, + name: "bid", + f: orderFulfillment{ + Order: NewOrder(111).WithBid(&BidOrder{ + Price: sdk.Coin{Denom: "price", Amount: sdkmath.NewInt(55)}, + BuyerSettlementFees: sdk.Coins{ + {Denom: "feea", Amount: sdkmath.NewInt(3)}, + {Denom: "feeb", Amount: sdkmath.NewInt(4)}, + }, + }), }, - expErr: "ask order 53 cannot pay \"-1fig\" in fees: negative amount", }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + expected := tc.f.GetHoldAmount() + var actual sdk.Coins + testFunc := func() { + actual = tc.f.GetHoldAmount() + } + require.NotPanics(t, testFunc, "GetHoldAmount()") + assert.Equal(t, expected, actual, "GetHoldAmount() result") + }) + } +} + +func TestOrderFulfillment_DistributeAssets(t *testing.T) { + newOF := func(order *Order, assetsUnfilled, assetsFilled int64, dists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ + Order: order, + AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), + AssetsFilledAmt: sdkmath.NewInt(assetsFilled), + } + if assetsUnfilled == 0 { + rv.AssetsUnfilledAmt = ZeroAmtAfterSub + } + if len(dists) > 0 { + rv.AssetDists = dists + } + return rv + + } + askOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithAsk(&AskOrder{Assets: sdk.NewInt64Coin("apple", 999)}) + return newOF(order, assetsUnfilled, assetsFilled, dists...) + } + bidOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithBid(&BidOrder{Assets: sdk.NewInt64Coin("apple", 999)}) + return newOF(order, assetsUnfilled, assetsFilled, dists...) + } + dist := func(addr string, amt int64) *distribution { + return &distribution{ + Address: addr, + Amount: sdkmath.NewInt(amt), + } + } + + tests := []struct { + name string + receiver *orderFulfillment + order OrderI + amount sdkmath.Int + expRes *orderFulfillment + expErr string + }{ { - name: "bid with negative fees to pay", - bidOFs: []*OrderFulfillment{ - { - Order: NewOrder(35).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 15), - Price: sdk.NewInt64Coin("plum", 42), - }), - AssetsFilledAmt: sdkmath.NewInt(15), - AssetDists: []*Distribution{{Address: "seller1", Amount: sdkmath.NewInt(15)}}, - PriceAppliedAmt: sdkmath.NewInt(42), - PriceDists: []*Distribution{{Address: "seller1", Amount: sdkmath.NewInt(42)}}, - FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(-1)}}, - }, - }, - expErr: "bid order 35 cannot pay \"-1fig\" in fees: negative amount", + name: "assets unfilled less than amount: ask, ask", + receiver: askOF(1, 5, 0), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(6), + expErr: "cannot fill ask order 1 having assets left \"5apple\" with \"6apple\" from ask order 2: overfill", }, { - name: "two asks, three bids", - askOFs: []*OrderFulfillment{ - { - Order: NewOrder(77).WithAsk(&AskOrder{ - Seller: "seller77", - Assets: sdk.NewInt64Coin("apple", 15), - Price: sdk.NewInt64Coin("plum", 42), - }), - AssetsFilledAmt: sdkmath.NewInt(15), - AssetDists: []*Distribution{ - {Address: "buyer5511", Amount: sdkmath.NewInt(15)}, - }, - PriceAppliedAmt: sdkmath.NewInt(43), - PriceDists: []*Distribution{ - {Address: "buyer5511", Amount: sdkmath.NewInt(30)}, - {Address: "buyer78", Amount: sdkmath.NewInt(12)}, - {Address: "buyer9001", Amount: sdkmath.NewInt(1)}, - }, - FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(11)}}, - }, - { - Order: NewOrder(3).WithAsk(&AskOrder{ - Seller: "seller3", - Assets: sdk.NewInt64Coin("apple", 43), - Price: sdk.NewInt64Coin("plum", 88), - }), - AssetsFilledAmt: sdkmath.NewInt(43), - AssetDists: []*Distribution{ - {Address: "buyer5511", Amount: sdkmath.NewInt(5)}, - {Address: "buyer78", Amount: sdkmath.NewInt(7)}, - {Address: "buyer9001", Amount: sdkmath.NewInt(31)}, - }, - PriceAppliedAmt: sdkmath.NewInt(90), - PriceDists: []*Distribution{ - {Address: "buyer78", Amount: sdkmath.NewInt(5)}, - {Address: "buyer9001", Amount: sdkmath.NewInt(83)}, - {Address: "buyer9001", Amount: sdkmath.NewInt(2)}, - }, - FeesToPay: nil, - }, - }, - bidOFs: []*OrderFulfillment{ - { - Order: NewOrder(5511).WithBid(&BidOrder{ - Buyer: "buyer5511", - Assets: sdk.NewInt64Coin("apple", 20), - Price: sdk.NewInt64Coin("plum", 30), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetDists: []*Distribution{ - {Address: "seller77", Amount: sdkmath.NewInt(15)}, - {Address: "seller3", Amount: sdkmath.NewInt(5)}, - }, - PriceAppliedAmt: sdkmath.NewInt(30), - PriceDists: []*Distribution{ - {Address: "seller77", Amount: sdkmath.NewInt(30)}, - }, - FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(10)}}, - }, - { - Order: NewOrder(78).WithBid(&BidOrder{ - Buyer: "buyer78", - Assets: sdk.NewInt64Coin("apple", 7), - Price: sdk.NewInt64Coin("plum", 15), - }), - AssetsFilledAmt: sdkmath.NewInt(7), - AssetDists: []*Distribution{ - {Address: "seller3", Amount: sdkmath.NewInt(7)}, - }, - PriceAppliedAmt: sdkmath.NewInt(15), - PriceDists: []*Distribution{ - {Address: "seller77", Amount: sdkmath.NewInt(12)}, - {Address: "seller3", Amount: sdkmath.NewInt(3)}, - }, - FeesToPay: sdk.Coins{sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(4)}}, - }, - { - Order: NewOrder(9001).WithBid(&BidOrder{ - Buyer: "buyer9001", - Assets: sdk.NewInt64Coin("apple", 31), - Price: sdk.NewInt64Coin("plum", 86), - }), - AssetsFilledAmt: sdkmath.NewInt(31), - AssetDists: []*Distribution{ - {Address: "seller3", Amount: sdkmath.NewInt(31)}, - }, - PriceAppliedAmt: sdkmath.NewInt(86), - PriceDists: []*Distribution{ - {Address: "seller3", Amount: sdkmath.NewInt(83)}, - {Address: "seller77", Amount: sdkmath.NewInt(2)}, - {Address: "seller3", Amount: sdkmath.NewInt(1)}, - }, - FeesToPay: nil, - }, - }, - expSettlement: &Settlement{ - Transfers: []*Transfer{ - { - Inputs: []banktypes.Input{{Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 15))}}, - Outputs: []banktypes.Output{{Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 15))}}, - }, - { - Inputs: []banktypes.Input{{Address: "seller3", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 43))}}, - Outputs: []banktypes.Output{ - {Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 5))}, - {Address: "buyer78", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 7))}, - {Address: "buyer9001", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 31))}, - }, - }, - { - Inputs: []banktypes.Input{{Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 30))}}, - Outputs: []banktypes.Output{{Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 30))}}, - }, - { - Inputs: []banktypes.Input{{Address: "buyer78", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 15))}}, - Outputs: []banktypes.Output{ - {Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 12))}, - {Address: "seller3", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 3))}, - }, - }, - { - Inputs: []banktypes.Input{{Address: "buyer9001", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 86))}}, - Outputs: []banktypes.Output{ - {Address: "seller3", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 84))}, - {Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 2))}, - }, - }, - }, - FeeInputs: []banktypes.Input{ - {Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("fig", 11))}, - {Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("fig", 10))}, - {Address: "buyer78", Coins: sdk.NewCoins(sdk.NewInt64Coin("grape", 4))}, - }, - }, - expErr: "", + name: "assets unfilled less than amount: ask, bid", + receiver: askOF(3, 5, 0), + order: NewOrder(4).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(6), + expErr: "cannot fill ask order 3 having assets left \"5apple\" with \"6apple\" from bid order 4: overfill", + }, + { + name: "assets unfilled less than amount: bid, ask", + receiver: bidOF(5, 5, 0), + order: NewOrder(6).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(6), + expErr: "cannot fill bid order 5 having assets left \"5apple\" with \"6apple\" from ask order 6: overfill", + }, + { + name: "assets unfilled less than amount: bid, bid", + receiver: bidOF(7, 5, 0), + order: NewOrder(8).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(6), + expErr: "cannot fill bid order 7 having assets left \"5apple\" with \"6apple\" from bid order 8: overfill", + }, + { + name: "assets unfilled equals amount: ask, bid", + receiver: askOF(1, 12345, 0), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(12345), + expRes: askOF(1, 0, 12345, dist("buYer", 12345)), + }, + { + name: "assets unfilled equals amount: bid, ask", + receiver: bidOF(1, 12345, 0), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(12345), + expRes: bidOF(1, 0, 12345, dist("seLLer", 12345)), + }, + { + name: "assets unfilled more than amount: ask, bid", + receiver: askOF(1, 12345, 0), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(300), + expRes: askOF(1, 12045, 300, dist("buYer", 300)), + }, + { + name: "assets unfilled more than amount: bid, ask", + receiver: bidOF(1, 12345, 0), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(300), + expRes: bidOF(1, 12045, 300, dist("seLLer", 300)), + }, + { + name: "already has 2 dists: ask, bid", + receiver: askOF(1, 12300, 45, dist("bbbbb", 40), dist("YYYYY", 5)), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(2000), + expRes: askOF(1, 10300, 2045, dist("bbbbb", 40), dist("YYYYY", 5), dist("buYer", 2000)), + }, + { + name: "already has 2 dists: bid, ask", + receiver: bidOF(1, 12300, 45, dist("sssss", 40), dist("LLLLL", 5)), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(2000), + expRes: bidOF(1, 10300, 2045, dist("sssss", 40), dist("LLLLL", 5), dist("seLLer", 2000)), + }, + { + name: "amt more than filled, ask, bid", + receiver: askOF(1, 45, 12300, dist("ssss", 12300)), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(45), + expRes: askOF(1, 0, 12345, dist("ssss", 12300), dist("buYer", 45)), + }, + { + name: "amt more than filled, bid, ask", + receiver: bidOF(1, 45, 12300, dist("ssss", 12300)), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(45), + expRes: bidOF(1, 0, 12345, dist("ssss", 12300), dist("seLLer", 45)), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - settlement := &Settlement{} - if tc.expSettlement == nil { - tc.expSettlement = &Settlement{} + orig := copyOrderFulfillment(tc.receiver) + if tc.expRes == nil { + tc.expRes = copyOrderFulfillment(tc.receiver) } var err error testFunc := func() { - err = buildTransfers(tc.askOFs, tc.bidOFs, settlement) + err = tc.receiver.DistributeAssets(tc.order, tc.amount) } - require.NotPanics(t, testFunc, "buildTransfers") - assertions.AssertErrorValue(t, err, tc.expErr, "buildTransfers error") - if !assert.Equal(t, tc.expSettlement, settlement, "settlement after buildTransfers") { - expTransStrs := make([]string, len(tc.expSettlement.Transfers)) - for i, t := range tc.expSettlement.Transfers { - expTransStrs[i] = fmt.Sprintf("[%d]%s", i, transferString(t)) - } - expTrans := strings.Join(expTransStrs, "\n") - actTransStrs := make([]string, len(tc.expSettlement.Transfers)) - for i, t := range settlement.Transfers { - actTransStrs[i] = fmt.Sprintf("[%d]%s", i, transferString(t)) - } - actTrans := strings.Join(actTransStrs, "\n") - assert.Equal(t, expTrans, actTrans, "transfers (as strings)") + require.NotPanics(t, testFunc, "distributeAssets") + assertions.AssertErrorValue(t, err, tc.expErr, "distributeAssets error") + if !assertEqualOrderFulfillments(t, tc.expRes, tc.receiver, "orderFulfillment after distributeAssets") { + t.Logf("Original: %s", orderFulfillmentString(orig)) + t.Logf(" Amount: %s", tc.amount) } }) } } -func TestPopulateFilled(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - askOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - Assets: coin(assetsAmt, "acorn"), - Price: coin(priceAmt, "prune"), - }) - } - bidOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Assets: coin(assetsAmt, "acorn"), - Price: coin(priceAmt, "prune"), - }) - } - newOF := func(order *Order, priceAppliedAmt int64, fees ...sdk.Coin) *OrderFulfillment { - rv := &OrderFulfillment{ +func TestDistributeAssets(t *testing.T) { + seller, buyer := "SelleR", "BuyeR" + newOF := func(order *Order, assetsUnfilled, assetsFilled int64, dists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ Order: order, - AssetsFilledAmt: order.GetAssets().Amount, - AssetsUnfilledAmt: sdkmath.ZeroInt(), - PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), - PriceLeftAmt: order.GetPrice().Amount.SubRaw(priceAppliedAmt), + AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), + AssetsFilledAmt: sdkmath.NewInt(assetsFilled), } - if len(fees) > 0 { - rv.FeesToPay = fees + if assetsUnfilled == 0 { + rv.AssetsUnfilledAmt = ZeroAmtAfterSub + } + if len(dists) > 0 { + rv.AssetDists = dists } return rv + } - filledOrder := func(order *Order, actualPrice int64, actualFees ...sdk.Coin) *FilledOrder { - rv := &FilledOrder{ - order: order, - actualPrice: coin(actualPrice, order.GetPrice().Denom), - } - if len(actualFees) > 0 { - rv.actualFees = actualFees + askOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithAsk(&AskOrder{ + Seller: seller, + Assets: sdk.NewInt64Coin("apple", 999), + }) + return newOF(order, assetsUnfilled, assetsFilled, dists...) + } + bidOF := func(orderID uint64, assetsUnfilled, assetsFilled int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithBid(&BidOrder{ + Buyer: buyer, + Assets: sdk.NewInt64Coin("apple", 999), + }) + return newOF(order, assetsUnfilled, assetsFilled, dists...) + } + dist := func(addr string, amt int64) *distribution { + return &distribution{ + Address: addr, + Amount: sdkmath.NewInt(amt), } - return rv } tests := []struct { - name string - askOFs []*OrderFulfillment - bidOFs []*OrderFulfillment - settlement *Settlement - expSettlement *Settlement + name string + of1 *orderFulfillment + of2 *orderFulfillment + amount sdkmath.Int + expOF1 *orderFulfillment + expOF2 *orderFulfillment + expErr string }{ { - name: "no partial", - askOFs: []*OrderFulfillment{ - newOF(askOrder(2001, 53, 87), 92, coin(12, "fig")), - newOF(askOrder(2002, 17, 33), 37), - newOF(askOrder(2003, 22, 56), 60), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(3001, 30, 40), 40), - newOF(bidOrder(3002, 27, 49), 49, coin(39, "fig")), - newOF(bidOrder(3003, 35, 100), 100), - }, - settlement: &Settlement{}, - expSettlement: &Settlement{ - FullyFilledOrders: []*FilledOrder{ - filledOrder(askOrder(2001, 53, 87), 92, coin(12, "fig")), - filledOrder(askOrder(2002, 17, 33), 37), - filledOrder(askOrder(2003, 22, 56), 60), - filledOrder(bidOrder(3001, 30, 40), 40), - filledOrder(bidOrder(3002, 27, 49), 49, coin(39, "fig")), - filledOrder(bidOrder(3003, 35, 100), 100), - }, - }, + name: "amount more than of1 unfilled: ask bid", + of1: askOF(1, 5, 0), + of2: bidOF(2, 6, 0), + amount: sdkmath.NewInt(6), + expOF2: bidOF(2, 0, 6, dist(seller, 6)), + expErr: "cannot fill ask order 1 having assets left \"5apple\" with \"6apple\" from bid order 2: overfill", }, { - name: "partial ask", - askOFs: []*OrderFulfillment{ - newOF(askOrder(2001, 53, 87), 92, coin(12, "fig")), - newOF(askOrder(2002, 17, 33), 37), - newOF(askOrder(2003, 22, 56), 60), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(3001, 30, 40), 40), - newOF(bidOrder(3002, 27, 49), 49, coin(39, "fig")), - newOF(bidOrder(3003, 35, 100), 100), - }, - settlement: &Settlement{PartialOrderLeft: askOrder(2002, 15, 63)}, - expSettlement: &Settlement{ - FullyFilledOrders: []*FilledOrder{ - filledOrder(askOrder(2001, 53, 87), 92, coin(12, "fig")), - filledOrder(askOrder(2003, 22, 56), 60), - filledOrder(bidOrder(3001, 30, 40), 40), - filledOrder(bidOrder(3002, 27, 49), 49, coin(39, "fig")), - filledOrder(bidOrder(3003, 35, 100), 100), - }, - PartialOrderFilled: filledOrder(askOrder(2002, 17, 33), 37), - PartialOrderLeft: askOrder(2002, 15, 63), - }, + name: "amount more than of1 unfilled: bid ask", + of1: bidOF(1, 5, 0), + of2: askOF(2, 6, 0), + amount: sdkmath.NewInt(6), + expOF2: askOF(2, 0, 6, dist(buyer, 6)), + expErr: "cannot fill bid order 1 having assets left \"5apple\" with \"6apple\" from ask order 2: overfill", }, { - name: "partial bid", - askOFs: []*OrderFulfillment{ - newOF(askOrder(2001, 53, 87), 92, coin(12, "fig")), - newOF(askOrder(2002, 17, 33), 37), - newOF(askOrder(2003, 22, 56), 60), - }, - bidOFs: []*OrderFulfillment{ - newOF(bidOrder(3001, 30, 40), 40), - newOF(bidOrder(3002, 27, 49), 49, coin(39, "fig")), - newOF(bidOrder(3003, 35, 100), 100), - }, - settlement: &Settlement{PartialOrderLeft: bidOrder(3003, 15, 63)}, - expSettlement: &Settlement{ - FullyFilledOrders: []*FilledOrder{ - filledOrder(askOrder(2001, 53, 87), 92, coin(12, "fig")), - filledOrder(askOrder(2002, 17, 33), 37), - filledOrder(askOrder(2003, 22, 56), 60), - filledOrder(bidOrder(3001, 30, 40), 40), - filledOrder(bidOrder(3002, 27, 49), 49, coin(39, "fig")), - }, - PartialOrderFilled: filledOrder(bidOrder(3003, 35, 100), 100), - PartialOrderLeft: bidOrder(3003, 15, 63), - }, + name: "amount more than of2 unfilled: ask, bid", + of1: askOF(1, 6, 0), + of2: bidOF(2, 5, 0), + amount: sdkmath.NewInt(6), + expOF1: askOF(1, 0, 6, dist(buyer, 6)), + expErr: "cannot fill bid order 2 having assets left \"5apple\" with \"6apple\" from ask order 1: overfill", + }, + { + name: "amount more than of2 unfilled: bid, ask", + of1: bidOF(1, 6, 0), + of2: askOF(2, 5, 0), + amount: sdkmath.NewInt(6), + expOF1: bidOF(1, 0, 6, dist(seller, 6)), + expErr: "cannot fill ask order 2 having assets left \"5apple\" with \"6apple\" from bid order 1: overfill", + }, + { + name: "amount more than both unfilled: ask, bid", + of1: askOF(1, 5, 0), + of2: bidOF(2, 4, 0), + amount: sdkmath.NewInt(6), + expErr: "cannot fill ask order 1 having assets left \"5apple\" with \"6apple\" from bid order 2: overfill" + "\n" + + "cannot fill bid order 2 having assets left \"4apple\" with \"6apple\" from ask order 1: overfill", + }, + { + name: "amount more than both unfilled: ask, bid", + of1: bidOF(1, 5, 0), + of2: askOF(2, 4, 0), + amount: sdkmath.NewInt(6), + expErr: "cannot fill bid order 1 having assets left \"5apple\" with \"6apple\" from ask order 2: overfill" + "\n" + + "cannot fill ask order 2 having assets left \"4apple\" with \"6apple\" from bid order 1: overfill", + }, + { + name: "ask bid", + of1: askOF(1, 10, 55, dist("bbb", 55)), + of2: bidOF(2, 10, 0), + amount: sdkmath.NewInt(9), + expOF1: askOF(1, 1, 64, dist("bbb", 55), dist(buyer, 9)), + expOF2: bidOF(2, 1, 9, dist(seller, 9)), + }, + { + name: "bid ask", + of1: bidOF(1, 10, 55, dist("sss", 55)), + of2: askOF(2, 10, 3, dist("bbb", 3)), + amount: sdkmath.NewInt(10), + expOF1: bidOF(1, 0, 65, dist("sss", 55), dist(seller, 10)), + expOF2: askOF(2, 0, 13, dist("bbb", 3), dist(buyer, 10)), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + origOF1 := copyOrderFulfillment(tc.of1) + origOF2 := copyOrderFulfillment(tc.of2) + if tc.expOF1 == nil { + tc.expOF1 = copyOrderFulfillment(tc.of1) + } + if tc.expOF2 == nil { + tc.expOF2 = copyOrderFulfillment(tc.of2) + } + + var err error testFunc := func() { - populateFilled(tc.askOFs, tc.bidOFs, tc.settlement) + err = distributeAssets(tc.of1, tc.of2, tc.amount) + } + require.NotPanics(t, testFunc, "distributeAssets") + assertions.AssertErrorValue(t, err, tc.expErr, "distributeAssets error") + if !assertEqualOrderFulfillments(t, tc.expOF1, tc.of1, "of1 after distributeAssets") { + t.Logf("Original: %s", orderFulfillmentString(origOF1)) + t.Logf(" Amount: %s", tc.amount) + } + if !assertEqualOrderFulfillments(t, tc.expOF2, tc.of2, "of2 after distributeAssets") { + t.Logf("Original: %s", orderFulfillmentString(origOF2)) + t.Logf(" Amount: %s", tc.amount) } - require.NotPanics(t, testFunc, "populateFilled") - assert.Equal(t, tc.expSettlement, tc.settlement, "settlement after populateFilled") }) } } -func TestGetAssetTransfer(t *testing.T) { - seller, buyer := "sally", "brandon" - assetDenom, priceDenom := "apple", "peach" - newOF := func(order *Order, assetsFilledAmt int64, assetDists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ +func TestOrderFulfillment_DistributePrice(t *testing.T) { + newOF := func(order *Order, priceLeft, priceApplied int64, dists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ Order: order, - AssetsFilledAmt: sdkmath.NewInt(assetsFilledAmt), + PriceLeftAmt: sdkmath.NewInt(priceLeft), + PriceAppliedAmt: sdkmath.NewInt(priceApplied), } - if len(assetDists) > 0 { - rv.AssetDists = assetDists + if priceLeft == 0 { + rv.PriceLeftAmt = ZeroAmtAfterSub + } + if len(dists) > 0 { + rv.PriceDists = dists } return rv + } - askOrder := func(orderID uint64) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - MarketId: 5555, - Seller: seller, - Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, - Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, - }) - } - bidOrder := func(orderID uint64) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - MarketId: 5555, - Buyer: buyer, - Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, - Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, - }) - } - dist := func(addr string, amount int64) *Distribution { - return &Distribution{Address: addr, Amount: sdkmath.NewInt(amount)} + askOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithAsk(&AskOrder{Price: sdk.NewInt64Coin("peach", 999)}) + return newOF(order, priceLeft, priceApplied, dists...) } - input := func(addr string, amount int64) banktypes.Input { - return banktypes.Input{ - Address: addr, - Coins: sdk.Coins{{Denom: assetDenom, Amount: sdkmath.NewInt(amount)}}, - } + bidOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithBid(&BidOrder{Price: sdk.NewInt64Coin("peach", 999)}) + return newOF(order, priceLeft, priceApplied, dists...) } - output := func(addr string, amount int64) banktypes.Output { - return banktypes.Output{ + dist := func(addr string, amt int64) *distribution { + return &distribution{ Address: addr, - Coins: sdk.Coins{{Denom: assetDenom, Amount: sdkmath.NewInt(amount)}}, + Amount: sdkmath.NewInt(amt), } } tests := []struct { - name string - f *OrderFulfillment - expTransfer *Transfer - expErr string - expPanic string + name string + receiver *orderFulfillment + order OrderI + amount sdkmath.Int + expRes *orderFulfillment + expErr string }{ { - name: "nil inside order", - f: newOF(NewOrder(975), 5, dist("five", 5)), - expPanic: nilSubTypeErr(975), - }, - { - name: "unknown inside order", - f: newOF(newUnknownOrder(974), 5, dist("five", 5)), - expPanic: unknownSubTypeErr(974), - }, - { - name: "assets filled negative: ask", - f: newOF(askOrder(159), -5), - expErr: "ask order 159 cannot be filled with \"-5apple\" assets: amount not positive", - }, - { - name: "assets filled negative: bid", - f: newOF(bidOrder(953), -5), - expErr: "bid order 953 cannot be filled with \"-5apple\" assets: amount not positive", - }, - { - name: "assets filled zero: ask", - f: newOF(askOrder(991), 0), - expErr: "ask order 991 cannot be filled with \"0apple\" assets: amount not positive", - }, - { - name: "assets filled zero: bid", - f: newOF(bidOrder(992), 0), - expErr: "bid order 992 cannot be filled with \"0apple\" assets: amount not positive", - }, - { - name: "asset dists has negative amount: ask", - f: newOF(askOrder(549), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), - expErr: "ask order 549 cannot have \"-2apple\" assets in a transfer: amount not positive", - }, - { - name: "asset dists has negative amount: bid", - f: newOF(bidOrder(545), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), - expErr: "bid order 545 cannot have \"-2apple\" assets in a transfer: amount not positive", - }, - { - name: "asset dists has zero: ask", - f: newOF(askOrder(683), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), - expErr: "ask order 683 cannot have \"0apple\" assets in a transfer: amount not positive", - }, - { - name: "asset dists has zero: bid", - f: newOF(bidOrder(777), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), - expErr: "bid order 777 cannot have \"0apple\" assets in a transfer: amount not positive", - }, - { - name: "asset dists sum less than assets filled: ask", - f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), - expErr: "ask order 8 assets filled \"10apple\" does not equal assets distributed \"9apple\"", - }, - { - name: "asset dists sum less than assets filled: bid", - f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), - expErr: "bid order 3 assets filled \"10apple\" does not equal assets distributed \"9apple\"", + name: "assets unfilled less than amount: ask, ask", + receiver: askOF(1, 5, 0), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(6), + expRes: askOF(1, -1, 6, dist("seLLer", 6)), }, { - name: "asset dists sum more than assets filled: ask", - f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), - expErr: "ask order 8 assets filled \"10apple\" does not equal assets distributed \"11apple\"", + name: "assets unfilled less than amount: ask, bid", + receiver: askOF(3, 5, 0), + order: NewOrder(4).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(6), + expRes: askOF(3, -1, 6, dist("buYer", 6)), }, { - name: "asset dists sum more than assets filled: bid", - f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), - expErr: "bid order 3 assets filled \"10apple\" does not equal assets distributed \"11apple\"", + name: "assets unfilled less than amount: bid, ask", + receiver: bidOF(5, 5, 0), + order: NewOrder(6).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(6), + expErr: "cannot fill bid order 5 having price left \"5peach\" to ask order 6 at a price of \"6peach\": overfill", }, { - name: "one dist: ask", - f: newOF(askOrder(12), 10, dist("ten", 10)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(seller, 10)}, - Outputs: []banktypes.Output{output("ten", 10)}, - }, + name: "assets unfilled less than amount: bid, bid", + receiver: bidOF(7, 5, 0), + order: NewOrder(8).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(6), + expErr: "cannot fill bid order 7 having price left \"5peach\" to bid order 8 at a price of \"6peach\": overfill", }, { - name: "one dist: bid", - f: newOF(bidOrder(13), 10, dist("ten", 10)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("ten", 10)}, - Outputs: []banktypes.Output{output(buyer, 10)}, - }, + name: "assets unfilled equals amount: ask, bid", + receiver: askOF(1, 12345, 0), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(12345), + expRes: askOF(1, 0, 12345, dist("buYer", 12345)), }, { - name: "two dists, different addresses: ask", - f: newOF(askOrder(2111), 20, dist("eleven", 11), dist("nine", 9)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(seller, 20)}, - Outputs: []banktypes.Output{output("eleven", 11), output("nine", 9)}, - }, + name: "assets unfilled equals amount: bid, ask", + receiver: bidOF(1, 12345, 0), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(12345), + expRes: bidOF(1, 0, 12345, dist("seLLer", 12345)), }, { - name: "two dists, different addresses: bid", - f: newOF(bidOrder(1222), 20, dist("eleven", 11), dist("nine", 9)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("eleven", 11), input("nine", 9)}, - Outputs: []banktypes.Output{output(buyer, 20)}, - }, + name: "assets unfilled more than amount: ask, bid", + receiver: askOF(1, 12345, 0), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(300), + expRes: askOF(1, 12045, 300, dist("buYer", 300)), }, { - name: "two dists, same addresses: ask", - f: newOF(askOrder(5353), 52, dist("billy", 48), dist("billy", 4)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(seller, 52)}, - Outputs: []banktypes.Output{output("billy", 52)}, - }, + name: "assets unfilled more than amount: bid, ask", + receiver: bidOF(1, 12345, 0), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(300), + expRes: bidOF(1, 12045, 300, dist("seLLer", 300)), }, { - name: "two dists, same addresses: bid", - f: newOF(bidOrder(3535), 52, dist("sol", 48), dist("sol", 4)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("sol", 52)}, - Outputs: []banktypes.Output{output(buyer, 52)}, - }, + name: "already has 2 dists: ask, bid", + receiver: askOF(1, 12300, 45, dist("bbbbb", 40), dist("YYYYY", 5)), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(2000), + expRes: askOF(1, 10300, 2045, dist("bbbbb", 40), dist("YYYYY", 5), dist("buYer", 2000)), }, { - name: "four dists: ask", - f: newOF(askOrder(99221), 33, - dist("buddy", 10), dist("brian", 13), dist("buddy", 8), dist("bella", 2)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(seller, 33)}, - Outputs: []banktypes.Output{output("buddy", 18), output("brian", 13), output("bella", 2)}, - }, + name: "already has 2 dists: bid, ask", + receiver: bidOF(1, 12300, 45, dist("sssss", 40), dist("LLLLL", 5)), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(2000), + expRes: bidOF(1, 10300, 2045, dist("sssss", 40), dist("LLLLL", 5), dist("seLLer", 2000)), }, { - name: "four dists: bid", - f: newOF(bidOrder(99221), 33, - dist("sydney", 10), dist("sarah", 2), dist("sydney", 8), dist("spencer", 13)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("sydney", 18), input("sarah", 2), input("spencer", 13)}, - Outputs: []banktypes.Output{output(buyer, 33)}, - }, + name: "amt more than filled, ask, bid", + receiver: askOF(1, 45, 12300, dist("ssss", 12300)), + order: NewOrder(2).WithBid(&BidOrder{Buyer: "buYer"}), + amount: sdkmath.NewInt(45), + expRes: askOF(1, 0, 12345, dist("ssss", 12300), dist("buYer", 45)), + }, + { + name: "amt more than filled, bid, ask", + receiver: bidOF(1, 45, 12300, dist("ssss", 12300)), + order: NewOrder(2).WithAsk(&AskOrder{Seller: "seLLer"}), + amount: sdkmath.NewInt(45), + expRes: bidOF(1, 0, 12345, dist("ssss", 12300), dist("seLLer", 45)), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - orig := copyOrderFulfillment(tc.f) - var transfer *Transfer + orig := copyOrderFulfillment(tc.receiver) + if tc.expRes == nil { + tc.expRes = copyOrderFulfillment(tc.receiver) + } var err error testFunc := func() { - transfer, err = getAssetTransfer(tc.f) + err = tc.receiver.DistributePrice(tc.order, tc.amount) } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getAssetTransfer") - assertions.AssertErrorValue(t, err, tc.expErr, "getAssetTransfer error") - if !assert.Equal(t, tc.expTransfer, transfer, "getAssetTransfer transfers") { - t.Logf(" Actual: %s", transferString(transfer)) - t.Logf("Expected: %s", transferString(tc.expTransfer)) + require.NotPanics(t, testFunc, "distributePrice") + assertions.AssertErrorValue(t, err, tc.expErr, "distributePrice error") + if !assertEqualOrderFulfillments(t, tc.expRes, tc.receiver, "orderFulfillment after distributePrice") { + t.Logf("Original: %s", orderFulfillmentString(orig)) + t.Logf(" Amount: %s", tc.amount) } - assertEqualOrderFulfillments(t, orig, tc.f, "OrderFulfillment before and after getAssetTransfer") }) } } -func TestGetPriceTransfer(t *testing.T) { - seller, buyer := "sally", "brandon" - assetDenom, priceDenom := "apple", "peach" - newOF := func(order *Order, priceAppliedAmt int64, priceDists ...*Distribution) *OrderFulfillment { - rv := &OrderFulfillment{ +func TestDistributePrice(t *testing.T) { + seller, buyer := "SelleR", "BuyeR" + newOF := func(order *Order, priceLeft, priceApplied int64, dists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ Order: order, - PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), + PriceLeftAmt: sdkmath.NewInt(priceLeft), + PriceAppliedAmt: sdkmath.NewInt(priceApplied), } - if len(priceDists) > 0 { - rv.PriceDists = priceDists + if priceLeft == 0 { + rv.PriceLeftAmt = ZeroAmtAfterSub + } + if len(dists) > 0 { + rv.PriceDists = dists } return rv + } - askOrder := func(orderID uint64) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - MarketId: 5555, - Seller: seller, - Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, - Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, + askOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithAsk(&AskOrder{ + Seller: seller, + Price: sdk.NewInt64Coin("peach", 999), }) + return newOF(order, priceLeft, priceApplied, dists...) } - bidOrder := func(orderID uint64) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - MarketId: 5555, - Buyer: buyer, - Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, - Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, + bidOF := func(orderID uint64, priceLeft, priceApplied int64, dists ...*distribution) *orderFulfillment { + order := NewOrder(orderID).WithBid(&BidOrder{ + Buyer: buyer, + Price: sdk.NewInt64Coin("peach", 999), }) + return newOF(order, priceLeft, priceApplied, dists...) } - dist := func(addr string, amount int64) *Distribution { - return &Distribution{Address: addr, Amount: sdkmath.NewInt(amount)} - } - input := func(addr string, amount int64) banktypes.Input { - return banktypes.Input{ - Address: addr, - Coins: sdk.Coins{{Denom: priceDenom, Amount: sdkmath.NewInt(amount)}}, - } - } - output := func(addr string, amount int64) banktypes.Output { - return banktypes.Output{ + dist := func(addr string, amt int64) *distribution { + return &distribution{ Address: addr, - Coins: sdk.Coins{{Denom: priceDenom, Amount: sdkmath.NewInt(amount)}}, + Amount: sdkmath.NewInt(amt), } } tests := []struct { - name string - f *OrderFulfillment - expTransfer *Transfer - expErr string - expPanic string + name string + of1 *orderFulfillment + of2 *orderFulfillment + amount sdkmath.Int + expOF1 *orderFulfillment + expOF2 *orderFulfillment + expErr string }{ { - name: "nil inside order", - f: newOF(NewOrder(975), 5, dist("five", 5)), - expPanic: nilSubTypeErr(975), - }, - { - name: "unknown inside order", - f: newOF(newUnknownOrder(974), 5, dist("five", 5)), - expPanic: unknownSubTypeErr(974), - }, - { - name: "price applied negative: ask", - f: newOF(askOrder(159), -5), - expErr: "ask order 159 cannot be filled at price \"-5peach\": amount not positive", - }, - { - name: "price applied negative: bid", - f: newOF(bidOrder(953), -5), - expErr: "bid order 953 cannot be filled at price \"-5peach\": amount not positive", - }, - { - name: "price applied zero: ask", - f: newOF(askOrder(991), 0), - expErr: "ask order 991 cannot be filled at price \"0peach\": amount not positive", - }, - { - name: "price applied zero: bid", - f: newOF(bidOrder(992), 0), - expErr: "bid order 992 cannot be filled at price \"0peach\": amount not positive", - }, - { - name: "price dists has negative amount: ask", - f: newOF(askOrder(549), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), - expErr: "ask order 549 cannot have price \"-2peach\" in a transfer: amount not positive", - }, - { - name: "price dists has negative amount: bid", - f: newOF(bidOrder(545), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), - expErr: "bid order 545 cannot have price \"-2peach\" in a transfer: amount not positive", - }, - { - name: "price dists has zero: ask", - f: newOF(askOrder(683), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), - expErr: "ask order 683 cannot have price \"0peach\" in a transfer: amount not positive", - }, - { - name: "price dists has zero: bid", - f: newOF(bidOrder(777), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), - expErr: "bid order 777 cannot have price \"0peach\" in a transfer: amount not positive", - }, - { - name: "price dists sum less than price applied: ask", - f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), - expErr: "ask order 8 price filled \"10peach\" does not equal price distributed \"9peach\"", - }, - { - name: "price dists sum less than price applied: bid", - f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), - expErr: "bid order 3 price filled \"10peach\" does not equal price distributed \"9peach\"", - }, - { - name: "price dists sum more than price applied: ask", - f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), - expErr: "ask order 8 price filled \"10peach\" does not equal price distributed \"11peach\"", - }, - { - name: "price dists sum more than price applied: bid", - f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), - expErr: "bid order 3 price filled \"10peach\" does not equal price distributed \"11peach\"", - }, - { - name: "one dist: ask", - f: newOF(askOrder(12), 10, dist("ten", 10)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("ten", 10)}, - Outputs: []banktypes.Output{output(seller, 10)}, - }, + name: "amount more than of1 unfilled: ask bid", + of1: askOF(1, 5, 0), + of2: bidOF(2, 6, 0), + amount: sdkmath.NewInt(6), + expOF1: askOF(1, -1, 6, dist(buyer, 6)), + expOF2: bidOF(2, 0, 6, dist(seller, 6)), }, { - name: "one dist: bid", - f: newOF(bidOrder(13), 10, dist("ten", 10)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(buyer, 10)}, - Outputs: []banktypes.Output{output("ten", 10)}, - }, + name: "amount more than of1 unfilled: bid ask", + of1: bidOF(1, 5, 0), + of2: askOF(2, 6, 0), + amount: sdkmath.NewInt(6), + expOF2: askOF(2, 0, 6, dist(buyer, 6)), + expErr: "cannot fill bid order 1 having price left \"5peach\" to ask order 2 at a price of \"6peach\": overfill", }, { - name: "two dists, different addresses: ask", - f: newOF(askOrder(2111), 20, dist("eleven", 11), dist("nine", 9)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("eleven", 11), input("nine", 9)}, - Outputs: []banktypes.Output{output(seller, 20)}, - }, + name: "amount more than of2 unfilled: ask, bid", + of1: askOF(1, 6, 0), + of2: bidOF(2, 5, 0), + amount: sdkmath.NewInt(6), + expOF1: askOF(1, 0, 6, dist(buyer, 6)), + expErr: "cannot fill bid order 2 having price left \"5peach\" to ask order 1 at a price of \"6peach\": overfill", }, { - name: "two dists, different addresses: bid", - f: newOF(bidOrder(1222), 20, dist("eleven", 11), dist("nine", 9)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(buyer, 20)}, - Outputs: []banktypes.Output{output("eleven", 11), output("nine", 9)}, - }, + name: "amount more than of2 unfilled: bid, ask", + of1: bidOF(1, 6, 0), + of2: askOF(2, 5, 0), + amount: sdkmath.NewInt(6), + expOF1: bidOF(1, 0, 6, dist(seller, 6)), + expOF2: askOF(2, -1, 6, dist(buyer, 6)), }, { - name: "two dists, same addresses: ask", - f: newOF(askOrder(5353), 52, dist("billy", 48), dist("billy", 4)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("billy", 52)}, - Outputs: []banktypes.Output{output(seller, 52)}, - }, + name: "amount more than both unfilled: ask, bid", + of1: askOF(1, 5, 0), + of2: bidOF(2, 4, 0), + amount: sdkmath.NewInt(6), + expOF1: askOF(1, -1, 6, dist(buyer, 6)), + expErr: "cannot fill bid order 2 having price left \"4peach\" to ask order 1 at a price of \"6peach\": overfill", }, { - name: "two dists, same addresses: bid", - f: newOF(bidOrder(3535), 52, dist("sol", 48), dist("sol", 4)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(buyer, 52)}, - Outputs: []banktypes.Output{output("sol", 52)}, - }, + name: "amount more than both unfilled: ask, bid", + of1: bidOF(1, 5, 0), + of2: askOF(2, 4, 0), + amount: sdkmath.NewInt(6), + expOF2: askOF(2, -2, 6, dist(buyer, 6)), + expErr: "cannot fill bid order 1 having price left \"5peach\" to ask order 2 at a price of \"6peach\": overfill", }, { - name: "four dists: ask", - f: newOF(askOrder(99221), 33, - dist("buddy", 10), dist("brian", 13), dist("buddy", 8), dist("bella", 2)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input("buddy", 18), input("brian", 13), input("bella", 2)}, - Outputs: []banktypes.Output{output(seller, 33)}, - }, + name: "ask bid", + of1: askOF(1, 10, 55, dist("bbb", 55)), + of2: bidOF(2, 10, 0), + amount: sdkmath.NewInt(9), + expOF1: askOF(1, 1, 64, dist("bbb", 55), dist(buyer, 9)), + expOF2: bidOF(2, 1, 9, dist(seller, 9)), }, { - name: "four dists: bid", - f: newOF(bidOrder(99221), 33, - dist("sydney", 10), dist("sarah", 2), dist("sydney", 8), dist("spencer", 13)), - expTransfer: &Transfer{ - Inputs: []banktypes.Input{input(buyer, 33)}, - Outputs: []banktypes.Output{output("sydney", 18), output("sarah", 2), output("spencer", 13)}, - }, + name: "bid ask", + of1: bidOF(1, 10, 55, dist("sss", 55)), + of2: askOF(2, 10, 3, dist("bbb", 3)), + amount: sdkmath.NewInt(10), + expOF1: bidOF(1, 0, 65, dist("sss", 55), dist(seller, 10)), + expOF2: askOF(2, 0, 13, dist("bbb", 3), dist(buyer, 10)), }, } - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - orig := copyOrderFulfillment(tc.f) - var transfer *Transfer + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + origOF1 := copyOrderFulfillment(tc.of1) + origOF2 := copyOrderFulfillment(tc.of2) + if tc.expOF1 == nil { + tc.expOF1 = copyOrderFulfillment(tc.of1) + } + if tc.expOF2 == nil { + tc.expOF2 = copyOrderFulfillment(tc.of2) + } + var err error testFunc := func() { - transfer, err = getPriceTransfer(tc.f) + err = distributePrice(tc.of1, tc.of2, tc.amount) } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getPriceTransfer") - assertions.AssertErrorValue(t, err, tc.expErr, "getPriceTransfer error") - if !assert.Equal(t, tc.expTransfer, transfer, "getPriceTransfer transfers") { - t.Logf(" Actual: %s", transferString(transfer)) - t.Logf("Expected: %s", transferString(tc.expTransfer)) + require.NotPanics(t, testFunc, "distributePrice") + assertions.AssertErrorValue(t, err, tc.expErr, "distributePrice error") + if !assertEqualOrderFulfillments(t, tc.expOF1, tc.of1, "of1 after distributePrice") { + t.Logf("Original: %s", orderFulfillmentString(origOF1)) + t.Logf(" Amount: %s", tc.amount) + } + if !assertEqualOrderFulfillments(t, tc.expOF2, tc.of2, "of2 after distributePrice") { + t.Logf("Original: %s", orderFulfillmentString(origOF2)) + t.Logf(" Amount: %s", tc.amount) } - assertEqualOrderFulfillments(t, orig, tc.f, "OrderFulfillment before and after getPriceTransfer") }) } } -func TestOrderFulfillment_Apply(t *testing.T) { - assetCoin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "acoin", Amount: sdkmath.NewInt(amt)} - } - priceCoin := func(amt int64) sdk.Coin { - return sdk.Coin{Denom: "pcoin", Amount: sdkmath.NewInt(amt)} +func TestOrderFulfillment_SplitOrder(t *testing.T) { + coin := func(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} } - askOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { - return NewOrder(orderID).WithAsk(&AskOrder{ - MarketId: 86420, - Seller: "seller", - Assets: assetCoin(assetsAmt), - Price: priceCoin(priceAmt), - }) + askOrder := func(orderID uint64, assetAmt, priceAmt int64, fees ...sdk.Coin) *Order { + askOrder := &AskOrder{ + MarketId: 55, + Seller: "samuel", + Assets: coin(assetAmt, "apple"), + Price: coin(priceAmt, "peach"), + AllowPartial: true, + } + if len(fees) > 1 { + t.Fatalf("a max of 1 fee can be provided to askOrder, actual: %s", sdk.Coins(fees)) + } + if len(fees) > 0 { + askOrder.SellerSettlementFlatFee = &fees[0] + } + return NewOrder(orderID).WithAsk(askOrder) } - bidOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - MarketId: 86420, - Buyer: "buyer", - Assets: assetCoin(assetsAmt), - Price: priceCoin(priceAmt), - }) + bidOrder := func(orderID uint64, assetAmt, priceAmt int64, fees ...sdk.Coin) *Order { + bidOrder := &BidOrder{ + MarketId: 55, + Buyer: "brian", + Assets: coin(assetAmt, "apple"), + Price: coin(priceAmt, "peach"), + AllowPartial: true, + } + if len(fees) > 0 { + bidOrder.BuyerSettlementFees = fees + } + return NewOrder(orderID).WithBid(bidOrder) } tests := []struct { - name string - receiver *OrderFulfillment - order *OrderFulfillment - assetsAmt sdkmath.Int - priceAmt sdkmath.Int - expErr string - expResult *OrderFulfillment + name string + receiver *orderFulfillment + expUnfilled *Order + expReceiver *orderFulfillment + expErr string }{ { - name: "fills order in full", - receiver: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(55), - }, - order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - assetsAmt: sdkmath.NewInt(20), - priceAmt: sdkmath.NewInt(55), - expResult: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - Assets: assetCoin(20), - Price: priceCoin(55), - }, - }, - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(55), - PriceLeftAmt: ZeroAmtAfterSub, + name: "order split error: ask", + receiver: &orderFulfillment{ + Order: askOrder(8, 10, 100), + AssetsFilledAmt: sdkmath.NewInt(-1), }, + expErr: "cannot split ask order 8 having asset \"10apple\" at \"-1apple\": amount filled not positive", }, { - name: "partially fills order", - receiver: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(55), - }, - order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - assetsAmt: sdkmath.NewInt(11), - priceAmt: sdkmath.NewInt(22), - expResult: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - Assets: assetCoin(11), - Price: priceCoin(22), - }, - }, - AssetsFilledAmt: sdkmath.NewInt(11), - AssetsUnfilledAmt: sdkmath.NewInt(9), - PriceAppliedAmt: sdkmath.NewInt(22), - PriceLeftAmt: sdkmath.NewInt(33), + name: "order split error: bid", + receiver: &orderFulfillment{ + Order: bidOrder(9, 10, 100), + AssetsFilledAmt: sdkmath.NewInt(-1), }, + expErr: "cannot split bid order 9 having asset \"10apple\" at \"-1apple\": amount filled not positive", }, { - name: "already partially filled, fills rest", - receiver: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: bidOrder(3, 60, 220)}, - Assets: assetCoin(9), - Price: priceCoin(33), - }, - }, + name: "okay: ask", + receiver: &orderFulfillment{ + Order: askOrder(17, 10, 100, coin(20, "fig")), AssetsFilledAmt: sdkmath.NewInt(9), - AssetsUnfilledAmt: sdkmath.NewInt(11), - PriceAppliedAmt: sdkmath.NewInt(33), - PriceLeftAmt: sdkmath.NewInt(22), - }, - order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - assetsAmt: sdkmath.NewInt(11), - priceAmt: sdkmath.NewInt(22), - expResult: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: bidOrder(3, 60, 220)}, - Assets: assetCoin(9), - Price: priceCoin(33), - }, - { - Order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - Assets: assetCoin(11), - Price: priceCoin(22), - }, - }, - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(55), - PriceLeftAmt: ZeroAmtAfterSub, - }, - }, - { - name: "ask assets overfill", - receiver: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(55), + AssetsUnfilledAmt: sdkmath.NewInt(1), + PriceAppliedAmt: sdkmath.NewInt(300), + PriceLeftAmt: sdkmath.NewInt(-200), }, - order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - assetsAmt: sdkmath.NewInt(21), - priceAmt: sdkmath.NewInt(55), - expErr: "cannot fill ask order 1 having assets left \"20acoin\" with \"21acoin\" from bid order 2: overfill", - }, - { - name: "bid assets overfill", - receiver: &OrderFulfillment{ - Order: bidOrder(1, 20, 55), - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(55), + expUnfilled: askOrder(17, 1, 10, coin(2, "fig")), + expReceiver: &orderFulfillment{ + Order: askOrder(17, 9, 90, coin(18, "fig")), + AssetsFilledAmt: sdkmath.NewInt(9), + AssetsUnfilledAmt: sdkmath.NewInt(0), + PriceAppliedAmt: sdkmath.NewInt(300), + PriceLeftAmt: sdkmath.NewInt(-210), }, - order: &OrderFulfillment{Order: askOrder(2, 40, 110)}, - assetsAmt: sdkmath.NewInt(21), - priceAmt: sdkmath.NewInt(55), - expErr: "cannot fill bid order 1 having assets left \"20acoin\" with \"21acoin\" from ask order 2: overfill", }, { - name: "ask price overfill", - receiver: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(55), - }, - order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - assetsAmt: sdkmath.NewInt(20), - priceAmt: sdkmath.NewInt(56), - expResult: &OrderFulfillment{ - Order: askOrder(1, 20, 55), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: bidOrder(2, 40, 110)}, - Assets: assetCoin(20), - Price: priceCoin(56), - }, - }, - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(56), - PriceLeftAmt: sdkmath.NewInt(-1), + name: "okay: bid", + receiver: &orderFulfillment{ + Order: bidOrder(19, 10, 100, coin(20, "fig")), + AssetsFilledAmt: sdkmath.NewInt(9), + AssetsUnfilledAmt: sdkmath.NewInt(1), + PriceAppliedAmt: sdkmath.NewInt(300), + PriceLeftAmt: sdkmath.NewInt(-200), }, - }, - { - name: "bid price overfill", - receiver: &OrderFulfillment{ - Order: bidOrder(1, 20, 55), - Splits: nil, - AssetsFilledAmt: sdkmath.ZeroInt(), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.ZeroInt(), - PriceLeftAmt: sdkmath.NewInt(55), + expUnfilled: bidOrder(19, 1, 10, coin(2, "fig")), + expReceiver: &orderFulfillment{ + Order: bidOrder(19, 9, 90, coin(18, "fig")), + AssetsFilledAmt: sdkmath.NewInt(9), + AssetsUnfilledAmt: sdkmath.NewInt(0), + PriceAppliedAmt: sdkmath.NewInt(300), + PriceLeftAmt: sdkmath.NewInt(-210), }, - order: &OrderFulfillment{Order: askOrder(2, 40, 110)}, - assetsAmt: sdkmath.NewInt(20), - priceAmt: sdkmath.NewInt(56), - expErr: "cannot fill bid order 1 having price left \"55pcoin\" to ask order 2 at a price of \"56pcoin\": overfill", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { orig := copyOrderFulfillment(tc.receiver) - if tc.expResult == nil { - tc.expResult = copyOrderFulfillment(tc.receiver) + if tc.expReceiver == nil { + tc.expReceiver = copyOrderFulfillment(tc.receiver) } + var unfilled *Order var err error testFunc := func() { - err = tc.receiver.Apply(tc.order, tc.assetsAmt, tc.priceAmt) + unfilled, err = tc.receiver.SplitOrder() } - require.NotPanics(t, testFunc, "Apply") - assertions.AssertErrorValue(t, err, tc.expErr, "Apply error") - if !assertEqualOrderFulfillments(t, tc.expResult, tc.receiver, "order fulfillment after .Apply") { + require.NotPanics(t, testFunc, "SplitOrder") + assertions.AssertErrorValue(t, err, tc.expErr, "SplitOrder error") + assert.Equalf(t, tc.expUnfilled, unfilled, "SplitOrder unfilled order") + if !assertEqualOrderFulfillments(t, tc.expReceiver, tc.receiver, "orderFulfillment after SplitOrder") { t.Logf("Original: %s", orderFulfillmentString(orig)) } }) } } -func TestOrderFulfillment_ApplyLeftoverPrice(t *testing.T) { - type testCase struct { - name string - receiver *OrderFulfillment - askSplit *OrderSplit - amt sdkmath.Int - expFulfillment *OrderFulfillment - expAskSplit *OrderSplit - expPanic string - } - - newTestCase := func(name string, bidSplitIndexes ...int) testCase { - // Picture a bid order with 150 assets at a cost of 5555 being split among 3 ask orders evenly (50 each). - // Each ask order has 53 to sell: 50 are coming from this bid order, and 1 and 2 each from two other bids. - // During initial splitting, the bid will pay each ask 5555 * 50 / 150 = 1851. - // 1851 * 3 = 5553, so there's 2 leftover. - // The other 3 are being bought for 30 each (90 total). - - bidOrderID := uint64(200) - bidOrder := NewOrder(bidOrderID).WithBid(&BidOrder{ - Price: sdk.NewInt64Coin("pcoin", 5555), - }) +func TestOrderFulfillment_AsFilledOrder(t *testing.T) { + askOrder := NewOrder(53).WithAsk(&AskOrder{ + MarketId: 765, + Seller: "mefirst", + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("peach", 88), + SellerSettlementFlatFee: &sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(6)}, + AllowPartial: true, + }) + bidOrder := NewOrder(9556).WithBid(&BidOrder{ + MarketId: 145, + Buyer: "gimmiegimmie", + Assets: sdk.NewInt64Coin("acorn", 1171), + Price: sdk.NewInt64Coin("prune", 5100), + BuyerSettlementFees: sdk.NewCoins(sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(14)}), + AllowPartial: false, + }) - tc := testCase{ - name: name, - receiver: &OrderFulfillment{ - Order: bidOrder, - PriceAppliedAmt: sdkmath.NewInt(5553), - PriceLeftAmt: sdkmath.NewInt(2), + tests := []struct { + name string + receiver orderFulfillment + expected *FilledOrder + }{ + { + name: "ask order", + receiver: orderFulfillment{ + Order: askOrder, + PriceAppliedAmt: sdkmath.NewInt(132), + FeesToPay: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(7)}), }, - amt: sdkmath.NewInt(2), - expFulfillment: &OrderFulfillment{ - Order: bidOrder, - PriceAppliedAmt: sdkmath.NewInt(5555), - PriceLeftAmt: ZeroAmtAfterSub, + expected: &FilledOrder{ + order: askOrder, + actualPrice: sdk.NewInt64Coin("peach", 132), + actualFees: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(7)}), }, - askSplit: &OrderSplit{ - Order: &OrderFulfillment{ - Order: NewOrder(1).WithAsk(&AskOrder{ - Price: sdk.NewInt64Coin("pcoin", 1500), - }), - PriceAppliedAmt: sdkmath.NewInt(1941), // 5555 * 50 / 150 = 1851 from main bid + 90 from the others. - PriceLeftAmt: sdkmath.NewInt(-441), // = 1500 - 1941 - }, - Price: sdk.NewInt64Coin("pcoin", 1851), + }, + { + name: "bid order", + receiver: orderFulfillment{ + Order: bidOrder, + PriceAppliedAmt: sdkmath.NewInt(5123), + FeesToPay: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(23)}), }, - expAskSplit: &OrderSplit{ - Order: &OrderFulfillment{ - Order: NewOrder(1).WithAsk(&AskOrder{ - Price: sdk.NewInt64Coin("pcoin", 1500), - }), - PriceAppliedAmt: sdkmath.NewInt(1943), - PriceLeftAmt: sdkmath.NewInt(-443), - }, - Price: sdk.NewInt64Coin("pcoin", 1853), + expected: &FilledOrder{ + order: bidOrder, + actualPrice: sdk.NewInt64Coin("prune", 5123), + actualFees: sdk.NewCoins(sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(23)}), }, - } + }, + } - bidSplits := []*OrderSplit{ - { - // This is the primary bid split that we'll be looking to update. - Order: &OrderFulfillment{Order: NewOrder(bidOrderID).WithBid(&BidOrder{})}, - Price: sdk.NewInt64Coin("pcoin", 1851), // == 5555 / 3 (truncated) + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var actual *FilledOrder + testFunc := func() { + actual = tc.receiver.AsFilledOrder() + } + require.NotPanics(t, testFunc, "AsFilledOrder()") + assert.Equal(t, tc.expected, actual, "AsFilledOrder() result") + }) + } +} + +func TestSumAssetsAndPrice(t *testing.T) { + coin := func(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} + } + askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { + return NewOrder(orderID).WithAsk(&AskOrder{ + Assets: assets, + Price: price, + }) + } + bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + Assets: assets, + Price: price, + }) + } + + tests := []struct { + name string + orders []*Order + expAssets sdk.Coins + expPrice sdk.Coins + expPanic string + }{ + { + name: "nil orders", + orders: nil, + expAssets: nil, + expPrice: nil, + }, + { + name: "empty orders", + orders: []*Order{}, + expAssets: nil, + expPrice: nil, + }, + { + name: "nil inside order", + orders: []*Order{ + askOrder(1, coin(2, "apple"), coin(3, "plum")), + NewOrder(4), + askOrder(5, coin(6, "apple"), coin(7, "plum")), }, - { - Order: &OrderFulfillment{Order: NewOrder(bidOrderID + 1).WithBid(&BidOrder{})}, - Price: sdk.NewInt64Coin("pcoin", 30), + expPanic: nilSubTypeErr(4), + }, + { + name: "unknown inside order", + orders: []*Order{ + askOrder(1, coin(2, "apple"), coin(3, "plum")), + newUnknownOrder(4), + askOrder(5, coin(6, "apple"), coin(7, "plum")), }, - { - Order: &OrderFulfillment{Order: NewOrder(bidOrderID + 2).WithBid(&BidOrder{})}, - Price: sdk.NewInt64Coin("pcoin", 60), + expPanic: unknownSubTypeErr(4), + }, + { + name: "one order, ask", + orders: []*Order{askOrder(1, coin(2, "apple"), coin(3, "plum"))}, + expAssets: sdk.NewCoins(coin(2, "apple")), + expPrice: sdk.NewCoins(coin(3, "plum")), + }, + { + name: "one order, bid", + orders: []*Order{bidOrder(1, coin(2, "apple"), coin(3, "plum"))}, + expAssets: sdk.NewCoins(coin(2, "apple")), + expPrice: sdk.NewCoins(coin(3, "plum")), + }, + { + name: "2 orders, same denoms", + orders: []*Order{ + askOrder(1, coin(2, "apple"), coin(3, "plum")), + bidOrder(4, coin(5, "apple"), coin(6, "plum")), }, - { - // This one is similar to [0], but with a different order id. - // It'll be used to test the case where the bid split isn't found. - Order: &OrderFulfillment{Order: NewOrder(bidOrderID + 3).WithBid(&BidOrder{})}, - Price: sdk.NewInt64Coin("pcoin", 1851), + expAssets: sdk.NewCoins(coin(7, "apple")), + expPrice: sdk.NewCoins(coin(9, "plum")), + }, + { + name: "2 orders, diff denoms", + orders: []*Order{ + bidOrder(1, coin(2, "avocado"), coin(3, "peach")), + askOrder(4, coin(5, "apple"), coin(6, "plum")), }, - } - - for _, i := range bidSplitIndexes { - tc.askSplit.Order.Splits = append(tc.askSplit.Order.Splits, bidSplits[i]) - if i == 0 { - tc.expAskSplit.Order.Splits = append(tc.expAskSplit.Order.Splits, &OrderSplit{ - Order: &OrderFulfillment{Order: NewOrder(bidOrderID).WithBid(&BidOrder{})}, - Price: sdk.NewInt64Coin("pcoin", 1853), // == 5555 * 50 / 150 + 2 leftover. - }) - } else { - tc.expAskSplit.Order.Splits = append(tc.expAskSplit.Order.Splits, copyOrderSplit(bidSplits[i])) - } - if i == 3 { - tc.expPanic = "could not apply leftover amount 2 from bid order 200 to ask order 1: bid split not found" - } - } - - return tc - } - - tests := []testCase{ - newTestCase("applies to first bid split", 0, 1, 2), - newTestCase("applies to second bid split", 2, 0, 1), - newTestCase("applies to third bid split", 1, 2, 0), - newTestCase("bid split not found", 1, 2, 3), + expAssets: sdk.NewCoins(coin(2, "avocado"), coin(5, "apple")), + expPrice: sdk.NewCoins(coin(3, "peach"), coin(6, "plum")), + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - origFulfillment := copyOrderFulfillment(tc.receiver) - origSplit := copyOrderSplit(tc.askSplit) - + var assets, price sdk.Coins testFunc := func() { - tc.receiver.ApplyLeftoverPrice(tc.askSplit, tc.amt) - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "ApplyLeftoverPrice") - if !assertEqualOrderFulfillments(t, tc.expFulfillment, tc.receiver, "OrderFulfillment after .ApplyLeftoverPrice") { - t.Logf("Original: %s", orderFulfillmentString(origFulfillment)) - } - if !assert.Equal(t, tc.expAskSplit, tc.askSplit, "askSplit after ApplyLeftoverPrice") { - t.Logf("Original askSplit: %s", orderSplitString(origSplit)) - t.Logf(" Actual askSplit: %s", orderSplitString(tc.askSplit)) - t.Logf("Expected askSplit: %s", orderSplitString(tc.expAskSplit)) + assets, price = sumAssetsAndPrice(tc.orders) } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "sumAssetsAndPrice") + assert.Equal(t, tc.expAssets.String(), assets.String(), "sumAssetsAndPrice") + assert.Equal(t, tc.expPrice.String(), price.String(), "sumAssetsAndPrice") }) } } -func TestOrderFulfillment_Finalize(t *testing.T) { - sdkNewInt64CoinP := func(denom string, amount int64) *sdk.Coin { - rv := sdk.NewInt64Coin(denom, amount) - return &rv - } - +func TestSumPriceLeft(t *testing.T) { tests := []struct { - name string - receiver *OrderFulfillment - sellerFeeRatio *FeeRatio - expResult *OrderFulfillment - expErr string + name string + fulfillments []*orderFulfillment + expected sdkmath.Int }{ { - name: "ask assets filled zero", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{}), - AssetsFilledAmt: sdkmath.ZeroInt(), - }, - expErr: "no assets filled in ask order 3", - }, - { - name: "ask assets filled negative", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{}), - AssetsFilledAmt: sdkmath.NewInt(-8), - }, - expErr: "no assets filled in ask order 3", - }, - { - name: "bid assets filled zero", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{}), - AssetsFilledAmt: sdkmath.ZeroInt(), - }, - expErr: "no assets filled in bid order 3", - }, - { - name: "bid assets filled negative", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{}), - AssetsFilledAmt: sdkmath.NewInt(-8), - }, - expErr: "no assets filled in bid order 3", + name: "nil fulfillments", + fulfillments: nil, + expected: sdkmath.NewInt(0), }, - { - name: "ask partial price not evenly divisible", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 101), - }), - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(40), - }, - expErr: `ask order 3 having assets "50apple" cannot be partially filled by "10apple": price "101pear" is not evenly divisible`, + name: "empty fulfillments", + fulfillments: []*orderFulfillment{}, + expected: sdkmath.NewInt(0), }, { - name: "bid partial price not evenly divisible", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 101), - }), - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(40), - }, - expErr: `bid order 3 having assets "50apple" cannot be partially filled by "10apple": price "101pear" is not evenly divisible`, + name: "one fulfillment, positive", + fulfillments: []*orderFulfillment{{PriceLeftAmt: sdkmath.NewInt(8)}}, + expected: sdkmath.NewInt(8), }, - { - name: "ask partial fees not evenly divisible", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 201), - }), - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(40), - }, - expErr: `ask order 3 having assets "50apple" cannot be partially filled by "10apple": fee "201fig" is not evenly divisible`, + name: "one fulfillment, zero", + fulfillments: []*orderFulfillment{{PriceLeftAmt: sdkmath.NewInt(0)}}, + expected: sdkmath.NewInt(0), }, { - name: "bid partial fees not evenly divisible", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 151)), - }), - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(40), - }, - expErr: `bid order 3 having assets "50apple" cannot be partially filled by "10apple": fee "151grape" is not evenly divisible`, + name: "one fulfillment, negative", + fulfillments: []*orderFulfillment{{PriceLeftAmt: sdkmath.NewInt(-3)}}, + expected: sdkmath.NewInt(-3), }, - { - name: "ask ratio calc error", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(40), - PriceAppliedAmt: sdkmath.NewInt(29), - PriceLeftAmt: sdkmath.NewInt(71), - }, - sellerFeeRatio: &FeeRatio{ - Price: sdk.NewInt64Coin("plum", 1), - Fee: sdk.NewInt64Coin("fig", 3), + name: "three fulfillments", + fulfillments: []*orderFulfillment{ + {PriceLeftAmt: sdkmath.NewInt(10)}, + {PriceLeftAmt: sdkmath.NewInt(200)}, + {PriceLeftAmt: sdkmath.NewInt(3000)}, }, - expErr: "could not calculate ask order 3 ratio fee: cannot apply ratio 1plum:3fig to price 29pear: incorrect price denom", + expected: sdkmath.NewInt(3210), }, - { - name: "ask full no ratio", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - }, - sellerFeeRatio: nil, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.ZeroInt(), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 200)), - OrderFeesLeft: nil, + name: "three fulfillments, one negative", + fulfillments: []*orderFulfillment{ + {PriceLeftAmt: sdkmath.NewInt(10)}, + {PriceLeftAmt: sdkmath.NewInt(-200)}, + {PriceLeftAmt: sdkmath.NewInt(3000)}, }, + expected: sdkmath.NewInt(2810), }, { - name: "ask full, exact ratio", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - }, - sellerFeeRatio: &FeeRatio{ - Price: sdk.NewInt64Coin("pear", 10), - Fee: sdk.NewInt64Coin("grape", 1), - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.ZeroInt(), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 11)), - OrderFeesLeft: nil, - }, - }, - { - name: "ask full, loose ratio", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - }, - sellerFeeRatio: &FeeRatio{ - Price: sdk.NewInt64Coin("pear", 13), - Fee: sdk.NewInt64Coin("grape", 1), - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.ZeroInt(), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 9)), - OrderFeesLeft: nil, - }, - }, - { - name: "ask partial no ratio", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - }, - sellerFeeRatio: nil, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(40), - PriceUnfilledAmt: sdkmath.NewInt(60), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 80)), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 120)), - }, - }, - { - name: "ask partial, exact ratio", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - }, - sellerFeeRatio: &FeeRatio{ - Price: sdk.NewInt64Coin("pear", 10), - Fee: sdk.NewInt64Coin("grape", 1), - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(40), - PriceUnfilledAmt: sdkmath.NewInt(60), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 80), sdk.NewInt64Coin("grape", 11)), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 120)), - }, - }, - { - name: "ask partial, loose ratio", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - }, - sellerFeeRatio: &FeeRatio{ - Price: sdk.NewInt64Coin("pear", 13), - Fee: sdk.NewInt64Coin("fig", 1), - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 200), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(110), - PriceLeftAmt: sdkmath.NewInt(-10), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(40), - PriceUnfilledAmt: sdkmath.NewInt(60), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 89)), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 120)), + name: "three fulfillments, all negative", + fulfillments: []*orderFulfillment{ + {PriceLeftAmt: sdkmath.NewInt(-10)}, + {PriceLeftAmt: sdkmath.NewInt(-200)}, + {PriceLeftAmt: sdkmath.NewInt(-3000)}, }, + expected: sdkmath.NewInt(-3210), }, - { - name: "bid full no leftovers", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 13)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(0), - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 100), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 13)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(0), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(0), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 13)), - OrderFeesLeft: nil, - }, - }, - { - name: "bid full with leftovers", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 1000), // 1000 / 50 = 20 per asset. - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 13)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(993), - PriceLeftAmt: sdkmath.NewInt(7), - Splits: []*OrderSplit{ - { - // This one will get 1 once the loop defaults to 1. - // So, 7 * split assets / 50 filled must be 0. - Order: &OrderFulfillment{ - Order: NewOrder(101).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }}, - AssetsFilledAmt: sdkmath.NewInt(5), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }, - { - // This one will not get anything more. - // It's in the same situation as the one above, but the leftover will run out first. - Order: &OrderFulfillment{ - Order: NewOrder(102).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }}, - AssetsFilledAmt: sdkmath.NewInt(4), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }, - { - // This one will get 4 in the first pass of the loop. - // I.e. 7 * split assets / 50 = 4. Assets 29 to 39 - Order: &OrderFulfillment{ - Order: NewOrder(103).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }}, - AssetsFilledAmt: sdkmath.NewInt(35), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(693), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }, - { - // This one will get 2 due to price left. - // I also need this one to have 7 * assets / 50 = 0, so it doesn't get 1 more on the first pass. - Order: &OrderFulfillment{ - Order: NewOrder(104).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 120), - }}, - AssetsFilledAmt: sdkmath.NewInt(6), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(120), - PriceLeftAmt: sdkmath.NewInt(2), - }, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 120), - }, - }, - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 50), - Price: sdk.NewInt64Coin("pear", 1000), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 13)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(1000), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - { - // This one should get 1 once the loop defaults to 1. - Order: &OrderFulfillment{ - Order: NewOrder(101).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 101), - }}, - AssetsFilledAmt: sdkmath.NewInt(5), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(101), - PriceLeftAmt: sdkmath.NewInt(-1), - }, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 101), - }, - { - // This one will not get anything more. - Order: &OrderFulfillment{ - Order: NewOrder(102).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }}, - AssetsFilledAmt: sdkmath.NewInt(4), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }, - { - // This one should get 4 in the first pass of the loop. - Order: &OrderFulfillment{ - Order: NewOrder(103).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 697), - }}, - AssetsFilledAmt: sdkmath.NewInt(35), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(697), - PriceLeftAmt: sdkmath.NewInt(-4), - }, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 697), - }, - { - // this one will get 2 due to price left. - // I also need this one to have 7 * assets / 50 = 0, so it doesn't get 1 more on the first pass. - Order: &OrderFulfillment{ - Order: NewOrder(104).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }}, - AssetsFilledAmt: sdkmath.NewInt(6), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(122), - PriceLeftAmt: ZeroAmtAfterSub, - }, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }, - }, - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(1000), - PriceUnfilledAmt: sdkmath.NewInt(0), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 13)), - OrderFeesLeft: nil, + name: "three fulfillments, all large", + fulfillments: []*orderFulfillment{ + {PriceLeftAmt: newInt(t, "3,000,000,000,000,000,000,000,000,000,000,300")}, + {PriceLeftAmt: newInt(t, "40,000,000,000,000,000,000,000,000,000,000,040")}, + {PriceLeftAmt: newInt(t, "500,000,000,000,000,000,000,000,000,000,000,005")}, }, + expected: newInt(t, "543,000,000,000,000,000,000,000,000,000,000,345"), }, { - name: "bid partial no leftovers", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 75), - Price: sdk.NewInt64Coin("pear", 150), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 300), sdk.NewInt64Coin("grape", 12)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(25), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(50), - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 75), - Price: sdk.NewInt64Coin("pear", 150), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 300), sdk.NewInt64Coin("grape", 12)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(25), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(50), - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(50), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 8)), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 100), sdk.NewInt64Coin("grape", 4)), - }, - }, - { - name: "bid partial with leftovers", - receiver: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 75), - Price: sdk.NewInt64Coin("pear", 1500), // 1020 / 51 = 20 per asset. - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 300), sdk.NewInt64Coin("grape", 12)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(25), - PriceAppliedAmt: sdkmath.NewInt(993), - PriceLeftAmt: sdkmath.NewInt(507), - Splits: []*OrderSplit{ - { - // This one will get 1 once the loop defaults to 1. - // So, 7 * split assets / 50 filled must be 0. - Order: &OrderFulfillment{ - Order: NewOrder(101).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }}, - AssetsFilledAmt: sdkmath.NewInt(5), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }, - { - // This one will not get anything more. - // It's in the same situation as the one above, but the leftover will run out first. - Order: &OrderFulfillment{ - Order: NewOrder(102).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }}, - AssetsFilledAmt: sdkmath.NewInt(4), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }, - { - // This one will get 4 in the first pass of the loop. - // I.e. 7 * split assets / 50 = 4. Assets 29 to 39 - Order: &OrderFulfillment{ - Order: NewOrder(103).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }}, - AssetsFilledAmt: sdkmath.NewInt(35), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(693), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }, - { - // This one will get 2 due to price left. - // I also need this one to have 7 * assets / 50 = 0, so it doesn't get 1 more on the first pass. - Order: &OrderFulfillment{ - Order: NewOrder(104).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 120), - }}, - AssetsFilledAmt: sdkmath.NewInt(6), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(120), - PriceLeftAmt: sdkmath.NewInt(2), - }, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 120), - }, - }, - }, - expResult: &OrderFulfillment{ - Order: NewOrder(3).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin("apple", 75), - Price: sdk.NewInt64Coin("pear", 1500), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 300), sdk.NewInt64Coin("grape", 12)), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(25), - PriceAppliedAmt: sdkmath.NewInt(1000), - PriceLeftAmt: sdkmath.NewInt(500), - Splits: []*OrderSplit{ - { - // This one should get 1 once the loop defaults to 1. - Order: &OrderFulfillment{ - Order: NewOrder(101).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 100), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 101), - }}, - AssetsFilledAmt: sdkmath.NewInt(5), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(101), - PriceLeftAmt: sdkmath.NewInt(-1), - }, - Assets: sdk.NewInt64Coin("apple", 5), - Price: sdk.NewInt64Coin("pear", 101), - }, - { - // This one will not get anything more. - Order: &OrderFulfillment{ - Order: NewOrder(102).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }}, - AssetsFilledAmt: sdkmath.NewInt(4), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: sdkmath.NewInt(0), - }, - Assets: sdk.NewInt64Coin("apple", 4), - Price: sdk.NewInt64Coin("pear", 80), - }, - { - // This one should get 4 in the first pass of the loop. - Order: &OrderFulfillment{ - Order: NewOrder(103).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 693), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 697), - }}, - AssetsFilledAmt: sdkmath.NewInt(35), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(697), - PriceLeftAmt: sdkmath.NewInt(-4), - }, - Assets: sdk.NewInt64Coin("apple", 35), - Price: sdk.NewInt64Coin("pear", 697), - }, - { - // this one will get 2 due to price left. - // I also need this one to have 7 * assets / 50 = 0, so it doesn't get 1 more on the first pass. - Order: &OrderFulfillment{ - Order: NewOrder(104).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }), - Splits: []*OrderSplit{{ - Order: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }}, - AssetsFilledAmt: sdkmath.NewInt(6), - AssetsUnfilledAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(122), - PriceLeftAmt: ZeroAmtAfterSub, - }, - Assets: sdk.NewInt64Coin("apple", 6), - Price: sdk.NewInt64Coin("pear", 122), - }, - }, - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(1000), - PriceUnfilledAmt: sdkmath.NewInt(500), - FeesToPay: sdk.NewCoins(sdk.NewInt64Coin("fig", 200), sdk.NewInt64Coin("grape", 8)), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 100), sdk.NewInt64Coin("grape", 4)), + name: "four fullfillments, small negative zero large", + fulfillments: []*orderFulfillment{ + {PriceLeftAmt: sdkmath.NewInt(654_789)}, + {PriceLeftAmt: sdkmath.NewInt(-789)}, + {PriceLeftAmt: sdkmath.NewInt(0)}, + {PriceLeftAmt: newInt(t, "543,000,000,000,000,000,000,000,000,000,000,345")}, }, + expected: newInt(t, "543,000,000,000,000,000,000,000,000,000,654,345"), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - orig := copyOrderFulfillment(tc.receiver) - if tc.expResult == nil { - tc.expResult = copyOrderFulfillment(tc.receiver) - tc.expResult.PriceFilledAmt = sdkmath.ZeroInt() - tc.expResult.PriceUnfilledAmt = sdkmath.ZeroInt() - } - - var err error + var actual sdkmath.Int testFunc := func() { - err = tc.receiver.Finalize(tc.sellerFeeRatio) - } - require.NotPanics(t, testFunc, "Finalize") - assertions.AssertErrorValue(t, err, tc.expErr, "Finalize error") - if !assertEqualOrderFulfillments(t, tc.expResult, tc.receiver, "receiver after Finalize") { - t.Logf("Original: %s", orderFulfillmentString(orig)) + actual = sumPriceLeft(tc.fulfillments) } + require.NotPanics(t, testFunc, "sumPriceLeft") + assert.Equal(t, tc.expected, actual, "sumPriceLeft") }) } } -func TestOrderFulfillment_Validate(t *testing.T) { - askOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { +func TestValidateCanSettle(t *testing.T) { + coin := func(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} + } + askOrder := func(orderID uint64, assets, price sdk.Coin) *Order { return NewOrder(orderID).WithAsk(&AskOrder{ - MarketId: 987, - Seller: "steve", - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, - Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(priceAmt)}, + Assets: assets, + Price: price, }) } - bidOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { + bidOrder := func(orderID uint64, assets, price sdk.Coin) *Order { return NewOrder(orderID).WithBid(&BidOrder{ - MarketId: 987, - Buyer: "bruce", - Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, - Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(priceAmt)}, + Assets: assets, + Price: price, }) } tests := []struct { - name string - f OrderFulfillment - expErr string + name string + askOrders []*Order + bidOrders []*Order + expErr string }{ { - name: "nil inside order", - f: OrderFulfillment{Order: NewOrder(8)}, - expErr: nilSubTypeErr(8), + name: "nil ask orders", + askOrders: nil, + bidOrders: []*Order{bidOrder(8, coin(10, "apple"), coin(11, "peach"))}, + expErr: "no ask orders provided", + }, + { + name: "no bid orders", + askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, + bidOrders: nil, + expErr: "no bid orders provided", + }, + { + name: "no orders", + askOrders: nil, + bidOrders: nil, + expErr: joinErrs("no ask orders provided", "no bid orders provided"), + }, + { + name: "bid order in asks", + askOrders: []*Order{ + askOrder(7, coin(10, "apple"), coin(11, "peach")), + bidOrder(8, coin(10, "apple"), coin(11, "peach")), + askOrder(9, coin(10, "apple"), coin(11, "peach")), + }, + bidOrders: []*Order{bidOrder(22, coin(10, "apple"), coin(11, "peach"))}, + expErr: "bid order 8 is not an ask order but is in the askOrders list at index 1", + }, + { + name: "nil inside order in asks", + askOrders: []*Order{ + askOrder(7, coin(10, "apple"), coin(11, "peach")), + NewOrder(8), + askOrder(9, coin(10, "apple"), coin(11, "peach")), + }, + bidOrders: []*Order{bidOrder(22, coin(10, "apple"), coin(11, "peach"))}, + expErr: " order 8 is not an ask order but is in the askOrders list at index 1", }, { - name: "unknown inside order", - f: OrderFulfillment{Order: newUnknownOrder(12)}, - expErr: unknownSubTypeErr(12), + name: "unknown inside order in asks", + askOrders: []*Order{ + askOrder(7, coin(10, "apple"), coin(11, "peach")), + newUnknownOrder(8), + askOrder(9, coin(10, "apple"), coin(11, "peach")), + }, + bidOrders: []*Order{bidOrder(22, coin(10, "apple"), coin(11, "peach"))}, + expErr: "*exchange.unknownOrderType order 8 is not an ask order but is in the askOrders list at index 1", }, { - name: "order price greater than price applied: ask", - f: OrderFulfillment{ - Order: askOrder(52, 10, 401), - PriceAppliedAmt: sdkmath.NewInt(400), - AssetsFilledAmt: sdkmath.NewInt(10), + name: "ask order in bids", + askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, + bidOrders: []*Order{ + bidOrder(21, coin(10, "apple"), coin(11, "peach")), + askOrder(22, coin(10, "apple"), coin(11, "peach")), + bidOrder(23, coin(10, "apple"), coin(11, "peach")), }, - expErr: "ask order 52 price \"401peach\" is more than price filled \"400peach\"", + expErr: "ask order 22 is not a bid order but is in the bidOrders list at index 1", }, { - name: "order price equal to price applied: ask", - f: OrderFulfillment{ - Order: askOrder(53, 10, 401), - PriceAppliedAmt: sdkmath.NewInt(401), - AssetsFilledAmt: sdkmath.NewInt(10), + name: "nil inside order in bids", + askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, + bidOrders: []*Order{ + bidOrder(21, coin(10, "apple"), coin(11, "peach")), + NewOrder(22), + bidOrder(23, coin(10, "apple"), coin(11, "peach")), }, - expErr: "", + expErr: " order 22 is not a bid order but is in the bidOrders list at index 1", }, { - name: "order price less than price applied: ask", - f: OrderFulfillment{ - Order: askOrder(54, 10, 401), - PriceAppliedAmt: sdkmath.NewInt(402), - AssetsFilledAmt: sdkmath.NewInt(10), + name: "unknown inside order in bids", + askOrders: []*Order{askOrder(7, coin(10, "apple"), coin(11, "peach"))}, + bidOrders: []*Order{ + bidOrder(21, coin(10, "apple"), coin(11, "peach")), + newUnknownOrder(22), + bidOrder(23, coin(10, "apple"), coin(11, "peach")), }, - expErr: "", + expErr: "*exchange.unknownOrderType order 22 is not a bid order but is in the bidOrders list at index 1", }, { - name: "order price greater than price applied: bid", - f: OrderFulfillment{ - Order: bidOrder(71, 17, 432), - PriceAppliedAmt: sdkmath.NewInt(431), - AssetsFilledAmt: sdkmath.NewInt(17), + name: "orders in wrong args", + askOrders: []*Order{ + askOrder(15, coin(10, "apple"), coin(11, "peach")), + bidOrder(16, coin(10, "apple"), coin(11, "peach")), + askOrder(17, coin(10, "apple"), coin(11, "peach")), }, - expErr: "bid order 71 price \"432peach\" is not equal to price filled \"431peach\"", + bidOrders: []*Order{ + bidOrder(91, coin(10, "apple"), coin(11, "peach")), + askOrder(92, coin(10, "apple"), coin(11, "peach")), + bidOrder(93, coin(10, "apple"), coin(11, "peach")), + }, + expErr: joinErrs( + "bid order 16 is not an ask order but is in the askOrders list at index 1", + "ask order 92 is not a bid order but is in the bidOrders list at index 1", + ), }, { - name: "order price equal to price applied: bid", - f: OrderFulfillment{ - Order: bidOrder(72, 17, 432), - PriceAppliedAmt: sdkmath.NewInt(432), - AssetsFilledAmt: sdkmath.NewInt(17), + name: "multiple ask asset denoms", + askOrders: []*Order{ + askOrder(55, coin(10, "apple"), coin(11, "peach")), + askOrder(56, coin(20, "avocado"), coin(22, "peach")), }, - expErr: "", + bidOrders: []*Order{ + bidOrder(61, coin(10, "apple"), coin(11, "peach")), + }, + expErr: "cannot settle with multiple ask order asset denoms \"10apple,20avocado\"", }, { - name: "order price less than price applied: bid", - f: OrderFulfillment{ - Order: bidOrder(73, 17, 432), - PriceAppliedAmt: sdkmath.NewInt(433), - AssetsFilledAmt: sdkmath.NewInt(17), + name: "multiple ask price denoms", + askOrders: []*Order{ + askOrder(55, coin(10, "apple"), coin(11, "peach")), + askOrder(56, coin(20, "apple"), coin(22, "plum")), }, - expErr: "bid order 73 price \"432peach\" is not equal to price filled \"433peach\"", + bidOrders: []*Order{ + bidOrder(61, coin(10, "apple"), coin(11, "peach")), + }, + expErr: "cannot settle with multiple ask order price denoms \"11peach,22plum\"", }, { - name: "order assets less than assets filled: ask", - f: OrderFulfillment{ - Order: askOrder(101, 53, 12345), - PriceAppliedAmt: sdkmath.NewInt(12345), - AssetsFilledAmt: sdkmath.NewInt(54), + name: "multiple bid asset denoms", + askOrders: []*Order{askOrder(88, coin(10, "apple"), coin(11, "peach"))}, + bidOrders: []*Order{ + bidOrder(12, coin(10, "apple"), coin(11, "peach")), + bidOrder(13, coin(20, "avocado"), coin(22, "peach")), }, - expErr: "ask order 101 assets \"53apple\" does not equal filled assets \"54apple\"", + expErr: "cannot settle with multiple bid order asset denoms \"10apple,20avocado\"", }, { - name: "order assets equal to assets filled: ask", - f: OrderFulfillment{ - Order: askOrder(202, 53, 12345), - PriceAppliedAmt: sdkmath.NewInt(12345), - AssetsFilledAmt: sdkmath.NewInt(53), + name: "multiple bid price denoms", + askOrders: []*Order{askOrder(88, coin(10, "apple"), coin(11, "peach"))}, + bidOrders: []*Order{ + bidOrder(12, coin(10, "apple"), coin(11, "peach")), + bidOrder(13, coin(20, "apple"), coin(22, "plum")), }, - expErr: "", + expErr: "cannot settle with multiple bid order price denoms \"11peach,22plum\"", }, { - name: "order assets more than assets filled: ask", - f: OrderFulfillment{ - Order: askOrder(303, 53, 12345), - PriceAppliedAmt: sdkmath.NewInt(12345), - AssetsFilledAmt: sdkmath.NewInt(52), + name: "all different denoms", + askOrders: []*Order{ + askOrder(55, coin(10, "apple"), coin(11, "peach")), + askOrder(56, coin(20, "avocado"), coin(22, "plum")), }, - expErr: "ask order 303 assets \"53apple\" does not equal filled assets \"52apple\"", + bidOrders: []*Order{ + bidOrder(12, coin(30, "acorn"), coin(33, "prune")), + bidOrder(13, coin(40, "acai"), coin(44, "pear")), + }, + expErr: joinErrs( + "cannot settle with multiple ask order asset denoms \"10apple,20avocado\"", + "cannot settle with multiple ask order price denoms \"11peach,22plum\"", + "cannot settle with multiple bid order asset denoms \"40acai,30acorn\"", + "cannot settle with multiple bid order price denoms \"44pear,33prune\"", + ), }, { - name: "order assets less than assets filled: bid", - f: OrderFulfillment{ - Order: bidOrder(404, 53, 12345), - PriceAppliedAmt: sdkmath.NewInt(12345), - AssetsFilledAmt: sdkmath.NewInt(54), + name: "different ask and bid asset denoms", + askOrders: []*Order{ + askOrder(15, coin(10, "apple"), coin(11, "peach")), + askOrder(16, coin(20, "apple"), coin(22, "peach")), }, - expErr: "bid order 404 assets \"53apple\" does not equal filled assets \"54apple\"", + bidOrders: []*Order{ + bidOrder(2001, coin(30, "acorn"), coin(33, "peach")), + bidOrder(2002, coin(40, "acorn"), coin(44, "peach")), + }, + expErr: "cannot settle different ask \"30apple\" and bid \"70acorn\" asset denoms", }, { - name: "order assets equal to assets filled: bid", - f: OrderFulfillment{ - Order: bidOrder(505, 53, 12345), - PriceAppliedAmt: sdkmath.NewInt(12345), - AssetsFilledAmt: sdkmath.NewInt(53), + name: "different ask and bid price denoms", + askOrders: []*Order{ + askOrder(15, coin(10, "apple"), coin(11, "peach")), + askOrder(16, coin(20, "apple"), coin(22, "peach")), }, - expErr: "", + bidOrders: []*Order{ + bidOrder(2001, coin(30, "apple"), coin(33, "plum")), + bidOrder(2002, coin(40, "apple"), coin(44, "plum")), + }, + expErr: "cannot settle different ask \"33peach\" and bid \"77plum\" price denoms", }, { - name: "order assets more than assets filled: bid", - f: OrderFulfillment{ - Order: bidOrder(606, 53, 12345), - PriceAppliedAmt: sdkmath.NewInt(12345), - AssetsFilledAmt: sdkmath.NewInt(52), + name: "different ask and bid denoms", + askOrders: []*Order{ + askOrder(15, coin(10, "apple"), coin(11, "peach")), + askOrder(16, coin(20, "apple"), coin(22, "peach")), }, - expErr: "bid order 606 assets \"53apple\" does not equal filled assets \"52apple\"", + bidOrders: []*Order{ + bidOrder(2001, coin(30, "acorn"), coin(33, "plum")), + bidOrder(2002, coin(40, "acorn"), coin(44, "plum")), + }, + expErr: joinErrs( + "cannot settle different ask \"30apple\" and bid \"70acorn\" asset denoms", + "cannot settle different ask \"33peach\" and bid \"77plum\" price denoms", + ), }, } @@ -6782,2077 +3833,1083 @@ func TestOrderFulfillment_Validate(t *testing.T) { t.Run(tc.name, func(t *testing.T) { var err error testFunc := func() { - err = tc.f.Validate() + err = validateCanSettle(tc.askOrders, tc.bidOrders) } - require.NotPanics(t, testFunc, "Validate") - assertions.AssertErrorValue(t, err, tc.expErr, "Validate error") + require.NotPanics(t, testFunc, "validateCanSettle") + assertions.AssertErrorValue(t, err, tc.expErr, "validateCanSettle error") }) } } -func TestOrderFulfillment_Validate2(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} +func TestAllocateAssets(t *testing.T) { + askOrder := func(orderID uint64, assetsAmt int64, seller string) *Order { + return NewOrder(orderID).WithAsk(&AskOrder{ + Seller: seller, + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, + }) + } + bidOrder := func(orderID uint64, assetsAmt int64, buyer string) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + Buyer: buyer, + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, + }) } - coinP := func(amount int64, denom string) *sdk.Coin { - return &sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} + newOF := func(order *Order, dists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ + Order: order, + AssetsFilledAmt: sdkmath.NewInt(0), + AssetsUnfilledAmt: order.GetAssets().Amount, + } + if len(dists) > 0 { + rv.AssetDists = dists + for _, d := range dists { + rv.AssetsFilledAmt = rv.AssetsFilledAmt.Add(d.Amount) + rv.AssetsUnfilledAmt = rv.AssetsUnfilledAmt.Sub(d.Amount) + } + } + return rv } - coins := func(amount int64, denom string) sdk.Coins { - return sdk.Coins{coin(amount, denom)} + dist := func(addr string, amount int64) *distribution { + return &distribution{Address: addr, Amount: sdkmath.NewInt(amount)} } tests := []struct { - name string - receiver OrderFulfillment - expErr string + name string + askOFs []*orderFulfillment + bidOFs []*orderFulfillment + expAskOFs []*orderFulfillment + expBidOfs []*orderFulfillment + expErr string }{ { - name: "nil order type", - receiver: OrderFulfillment{Order: NewOrder(2)}, - expErr: nilSubTypeErr(2), + name: "one ask, one bid: both full", + askOFs: []*orderFulfillment{newOF(askOrder(5, 10, "seller"))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(6, 10, "buyer"))}, + expAskOFs: []*orderFulfillment{newOF(askOrder(5, 10, "seller"), dist("buyer", 10))}, + expBidOfs: []*orderFulfillment{newOF(bidOrder(6, 10, "buyer"), dist("seller", 10))}, }, { - name: "unknown order type", - receiver: OrderFulfillment{Order: newUnknownOrder(3)}, - expErr: unknownSubTypeErr(3), + name: "one ask, one bid: ask partial", + askOFs: []*orderFulfillment{newOF(askOrder(5, 11, "seller"))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(16, 10, "buyer"))}, + expAskOFs: []*orderFulfillment{newOF(askOrder(5, 11, "seller"), dist("buyer", 10))}, + expBidOfs: []*orderFulfillment{newOF(bidOrder(16, 10, "buyer"), dist("seller", 10))}, }, { - name: "not finalized, ask", - receiver: OrderFulfillment{ - Order: NewOrder(4).WithAsk(&AskOrder{}), - IsFinalized: false, - }, - expErr: "fulfillment for ask order 4 has not been finalized", + name: "one ask, one bid: bid partial", + askOFs: []*orderFulfillment{newOF(askOrder(15, 10, "seller"))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(6, 11, "buyer"))}, + expAskOFs: []*orderFulfillment{newOF(askOrder(15, 10, "seller"), dist("buyer", 10))}, + expBidOfs: []*orderFulfillment{newOF(bidOrder(6, 11, "buyer"), dist("seller", 10))}, }, { - name: "not finalized, bid", - receiver: OrderFulfillment{ - Order: NewOrder(4).WithBid(&BidOrder{}), - IsFinalized: false, + name: "one ask, two bids: last bid not touched", + askOFs: []*orderFulfillment{newOF(askOrder(22, 10, "seller"))}, + bidOFs: []*orderFulfillment{ + newOF(bidOrder(64, 12, "buyer64")), + newOF(bidOrder(78, 1, "buyer78")), }, - expErr: "fulfillment for bid order 4 has not been finalized", - }, - - { - name: "assets unfilled negative", - receiver: OrderFulfillment{ - Order: NewOrder(5).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(-12), - AssetsFilledAmt: sdkmath.NewInt(97), + expAskOFs: []*orderFulfillment{newOF(askOrder(22, 10, "seller"), dist("buyer64", 10))}, + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(64, 12, "buyer64"), dist("seller", 10)), + newOF(bidOrder(78, 1, "buyer78")), }, - expErr: "ask order 5 having assets \"55apple\" has negative assets left \"-12apple\" after filling \"97apple\"", }, { - name: "assets filled zero", - receiver: OrderFulfillment{ - Order: NewOrder(6).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(55), - AssetsFilledAmt: sdkmath.NewInt(0), + name: "two asks, one bids: last ask not touched", + askOFs: []*orderFulfillment{ + newOF(askOrder(888, 10, "seller888")), + newOF(askOrder(999, 10, "seller999")), }, - expErr: "cannot fill non-positive assets \"0apple\" on bid order 6 having assets \"55apple\"", - }, - { - name: "assets filled negative", - receiver: OrderFulfillment{ - Order: NewOrder(7).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(56), - AssetsFilledAmt: sdkmath.NewInt(-1), + bidOFs: []*orderFulfillment{newOF(bidOrder(6, 10, "buyer"))}, + expAskOFs: []*orderFulfillment{ + newOF(askOrder(888, 10, "seller888"), dist("buyer", 10)), + newOF(askOrder(999, 10, "seller999")), }, - expErr: "cannot fill non-positive assets \"-1apple\" on ask order 7 having assets \"55apple\"", + expBidOfs: []*orderFulfillment{newOF(bidOrder(6, 10, "buyer"), dist("seller888", 10))}, }, { - name: "assets tracked too low", - receiver: OrderFulfillment{ - Order: NewOrder(8).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(44), - AssetsFilledAmt: sdkmath.NewInt(10), + name: "two asks, three bids: both full", + askOFs: []*orderFulfillment{ + newOF(askOrder(101, 15, "seller101")), + newOF(askOrder(102, 25, "seller102")), + }, + bidOFs: []*orderFulfillment{ + newOF(bidOrder(103, 10, "buyer103")), + newOF(bidOrder(104, 8, "buyer104")), + newOF(bidOrder(105, 22, "buyer105")), }, - expErr: "tracked assets \"54apple\" does not equal bid order 8 assets \"55apple\"", - }, - { - name: "assets tracked too high", - receiver: OrderFulfillment{ - Order: NewOrder(8).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(44), - AssetsFilledAmt: sdkmath.NewInt(12), + expAskOFs: []*orderFulfillment{ + newOF(askOrder(101, 15, "seller101"), dist("buyer103", 10), dist("buyer104", 5)), + newOF(askOrder(102, 25, "seller102"), dist("buyer104", 3), dist("buyer105", 22)), }, - expErr: "tracked assets \"56apple\" does not equal ask order 8 assets \"55apple\"", - }, - - { - name: "price left equals order price", - receiver: OrderFulfillment{ - Order: NewOrder(19).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98789), + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(103, 10, "buyer103"), dist("seller101", 10)), + newOF(bidOrder(104, 8, "buyer104"), dist("seller101", 5), dist("seller102", 3)), + newOF(bidOrder(105, 22, "buyer105"), dist("seller102", 22)), }, - expErr: "price left \"98789plum\" is not less than ask order 19 price \"98789plum\"", }, { - name: "price left more than order price", - receiver: OrderFulfillment{ - Order: NewOrder(20).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98790), + name: "two asks, three bids: ask partial", + askOFs: []*orderFulfillment{ + newOF(askOrder(101, 15, "seller101")), + newOF(askOrder(102, 26, "seller102")), }, - expErr: "price left \"98790plum\" is not less than bid order 20 price \"98789plum\"", - }, - { - name: "price applied zero", - receiver: OrderFulfillment{ - Order: NewOrder(21).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98788), - PriceAppliedAmt: sdkmath.NewInt(0), + bidOFs: []*orderFulfillment{ + newOF(bidOrder(103, 10, "buyer103")), + newOF(bidOrder(104, 8, "buyer104")), + newOF(bidOrder(105, 22, "buyer105")), }, - expErr: "cannot apply non-positive price \"0plum\" to bid order 21 having price \"98789plum\"", - }, - { - name: "price applied negative", - receiver: OrderFulfillment{ - Order: NewOrder(22).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98788), - PriceAppliedAmt: sdkmath.NewInt(-1), + expAskOFs: []*orderFulfillment{ + newOF(askOrder(101, 15, "seller101"), dist("buyer103", 10), dist("buyer104", 5)), + newOF(askOrder(102, 26, "seller102"), dist("buyer104", 3), dist("buyer105", 22)), }, - expErr: "cannot apply non-positive price \"-1plum\" to ask order 22 having price \"98789plum\"", - }, - { - name: "price tracked too low", - receiver: OrderFulfillment{ - Order: NewOrder(23).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(788), + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(103, 10, "buyer103"), dist("seller101", 10)), + newOF(bidOrder(104, 8, "buyer104"), dist("seller101", 5), dist("seller102", 3)), + newOF(bidOrder(105, 22, "buyer105"), dist("seller102", 22)), }, - expErr: "tracked price \"98788plum\" does not equal ask order 23 price \"98789plum\"", }, { - name: "price tracked too high", - receiver: OrderFulfillment{ - Order: NewOrder(24).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98001), - PriceAppliedAmt: sdkmath.NewInt(789), + name: "two asks, three bids: bid partial", + askOFs: []*orderFulfillment{ + newOF(askOrder(101, 15, "seller101")), + newOF(askOrder(102, 25, "seller102")), }, - expErr: "tracked price \"98790plum\" does not equal bid order 24 price \"98789plum\"", - }, - { - name: "price filled zero", - receiver: OrderFulfillment{ - Order: NewOrder(25).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(98789), - PriceFilledAmt: sdkmath.NewInt(0), - }, - expErr: "cannot fill ask order 25 having price \"98789plum\" with non-positive price \"0plum\"", - }, - { - name: "price filled negative", - receiver: OrderFulfillment{ - Order: NewOrder(26).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(98790), - PriceFilledAmt: sdkmath.NewInt(-1), - }, - expErr: "cannot fill bid order 26 having price \"98789plum\" with non-positive price \"-1plum\"", - }, - { - name: "total price too low", - receiver: OrderFulfillment{ - Order: NewOrder(27).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8788), - }, - expErr: "filled price \"8788plum\" plus unfilled price \"90000plum\" does not equal order price \"98789plum\" for ask order 27", - }, - { - name: "total price too high", - receiver: OrderFulfillment{ - Order: NewOrder(28).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90001), - PriceFilledAmt: sdkmath.NewInt(8789), - }, - expErr: "filled price \"8789plum\" plus unfilled price \"90001plum\" does not equal order price \"98789plum\" for bid order 28", - }, - { - name: "price unfilled negative", - receiver: OrderFulfillment{ - Order: NewOrder(29).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(-1), - PriceFilledAmt: sdkmath.NewInt(98790), + bidOFs: []*orderFulfillment{ + newOF(bidOrder(103, 10, "buyer103")), + newOF(bidOrder(104, 8, "buyer104")), + newOF(bidOrder(105, 23, "buyer105")), }, - expErr: "ask order 29 having price \"98789plum\" has negative price \"-1plum\" after filling \"98790plum\"", - }, - - { - name: "nil splits", - receiver: OrderFulfillment{ - Order: NewOrder(100).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: nil, - }, - expErr: "no splits applied to bid order 100", - }, - { - name: "empty splits", - receiver: OrderFulfillment{ - Order: NewOrder(101).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{}, - }, - expErr: "no splits applied to ask order 101", - }, - { - name: "multiple asset denoms in splits", - receiver: OrderFulfillment{ - Order: NewOrder(102).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(5, "acai"), Price: coin(89, "plum")}, - }, + expAskOFs: []*orderFulfillment{ + newOF(askOrder(101, 15, "seller101"), dist("buyer103", 10), dist("buyer104", 5)), + newOF(askOrder(102, 25, "seller102"), dist("buyer104", 3), dist("buyer105", 22)), }, - expErr: "multiple asset denoms \"5acai,3apple\" in splits applied to bid order 102 having assets \"55apple\"", - }, - { - name: "wrong splits assets denom", - receiver: OrderFulfillment{ - Order: NewOrder(103).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "acai"), Price: coin(700, "plum")}, - {Assets: coin(5, "acai"), Price: coin(89, "plum")}, - }, + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(103, 10, "buyer103"), dist("seller101", 10)), + newOF(bidOrder(104, 8, "buyer104"), dist("seller101", 5), dist("seller102", 3)), + newOF(bidOrder(105, 23, "buyer105"), dist("seller102", 22)), }, - expErr: "splits asset denom \"8acai\" does not equal order assets denom \"55apple\" on ask order 103", }, { - name: "splits assets total too low", - receiver: OrderFulfillment{ - Order: NewOrder(104).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(800, "plum")}, - {Assets: coin(6, "apple"), Price: coin(89, "plum")}, - }, - }, - expErr: "splits asset total \"9apple\" does not equal filled assets \"10apple\" on bid order 104", + name: "negative ask assets unfilled", + askOFs: []*orderFulfillment{newOF(askOrder(101, 10, "seller"), dist("buyerx", 11))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(102, 10, "buyer"))}, + expErr: "cannot fill ask order 101 having assets left \"-1apple\" with bid order 102 having " + + "assets left \"10apple\": zero or negative assets left", }, { - name: "splits assets total too high", - receiver: OrderFulfillment{ - Order: NewOrder(105).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(8, "apple"), Price: coin(89, "plum")}, - }, - }, - expErr: "splits asset total \"11apple\" does not equal filled assets \"10apple\" on ask order 105", + name: "negative bid assets unfilled", + askOFs: []*orderFulfillment{newOF(askOrder(101, 10, "seller"))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(102, 10, "buyer"), dist("sellerx", 11))}, + expErr: "cannot fill ask order 101 having assets left \"10apple\" with bid order 102 having " + + "assets left \"-1apple\": zero or negative assets left", }, - { - name: "multiple price denoms in splits", - receiver: OrderFulfillment{ - Order: NewOrder(106).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "potato")}, - {Assets: coin(7, "apple"), Price: coin(89, "plum")}, - }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + origAskOFs := copyOrderFulfillments(tc.askOFs) + origBidOFs := copyOrderFulfillments(tc.bidOFs) + + var err error + testFunc := func() { + err = allocateAssets(tc.askOFs, tc.bidOFs) + } + require.NotPanics(t, testFunc, "allocateAssets") + assertions.AssertErrorValue(t, err, tc.expErr, "allocateAssets error") + if len(tc.expErr) > 0 { + return + } + if !assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after allocateAssets") { + t.Logf("Original: %s", orderFulfillmentsString(origAskOFs)) + } + if !assertEqualOrderFulfillmentSlices(t, tc.expBidOfs, tc.bidOFs, "bidOFs after allocateAssets") { + t.Logf("Original: %s", orderFulfillmentsString(origBidOFs)) + } + }) + } +} + +func TestGetFulfillmentAssetsAmt(t *testing.T) { + newAskOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *orderFulfillment { + return &orderFulfillment{ + Order: NewOrder(orderID).WithAsk(&AskOrder{ + Assets: sdk.NewInt64Coin(assetDenom, 999), + }), + AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), + } + } + newBidOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *orderFulfillment { + return &orderFulfillment{ + Order: NewOrder(orderID).WithBid(&BidOrder{ + Assets: sdk.NewInt64Coin(assetDenom, 999), + }), + AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), + } + } + + cases := []struct { + name string + of1Unfilled int64 + of2Unfilled int64 + expAmt int64 + }{ + {name: "of1 zero", of1Unfilled: 0, of2Unfilled: 3, expAmt: 0}, + {name: "of1 negative", of1Unfilled: -4, of2Unfilled: 3, expAmt: 0}, + {name: "of2 zero", of1Unfilled: 5, of2Unfilled: 0, expAmt: 0}, + {name: "of2 negative", of1Unfilled: 5, of2Unfilled: -6, expAmt: 0}, + {name: "equal", of1Unfilled: 8, of2Unfilled: 8, expAmt: 8}, + {name: "of1 has fewer", of1Unfilled: 9, of2Unfilled: 10, expAmt: 9}, + {name: "of2 has fewer", of1Unfilled: 12, of2Unfilled: 11, expAmt: 11}, + } + + type testCase struct { + name string + of1 *orderFulfillment + of2 *orderFulfillment + expAmt sdkmath.Int + expErr string + } + + tests := make([]testCase, 0, len(cases)*4) + + for _, c := range cases { + newTests := []testCase{ + { + name: "ask bid " + c.name, + of1: newAskOF(1, c.of1Unfilled, "one"), + of2: newBidOF(2, c.of2Unfilled, "two"), + expAmt: sdkmath.NewInt(c.expAmt), }, - expErr: "multiple price denoms \"89plum,700potato\" in splits applied to bid order 106 having price \"98789plum\"", - }, - { - name: "wrong splits price denom", - receiver: OrderFulfillment{ - Order: NewOrder(107).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "potato")}, - {Assets: coin(7, "apple"), Price: coin(89, "potato")}, - }, + { + name: "bid ask " + c.name, + of1: newBidOF(1, c.of1Unfilled, "one"), + of2: newAskOF(2, c.of2Unfilled, "two"), + expAmt: sdkmath.NewInt(c.expAmt), }, - expErr: "splits price denom \"789potato\" does not equal order price denom \"98789plum\" on ask order 107", - }, - { - name: "splits price total too low", - receiver: OrderFulfillment{ - Order: NewOrder(108).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(7, "apple"), Price: coin(88, "plum")}, - }, + { + name: "ask ask " + c.name, + of1: newAskOF(1, c.of1Unfilled, "one"), + of2: newAskOF(2, c.of2Unfilled, "two"), + expAmt: sdkmath.NewInt(c.expAmt), }, - expErr: "splits price total \"788plum\" does not equal filled price \"789plum\" on bid order 108", - }, - { - name: "splits price total too high", - receiver: OrderFulfillment{ - Order: NewOrder(109).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(701, "plum")}, - {Assets: coin(7, "apple"), Price: coin(89, "plum")}, - }, + { + name: "bid bid " + c.name, + of1: newBidOF(1, c.of1Unfilled, "one"), + of2: newBidOF(2, c.of2Unfilled, "two"), + expAmt: sdkmath.NewInt(c.expAmt), }, - expErr: "splits price total \"790plum\" does not equal filled price \"789plum\" on ask order 109", + } + if c.expAmt == 0 { + newTests[0].expErr = fmt.Sprintf("cannot fill ask order 1 having assets left \"%done\" with bid "+ + "order 2 having assets left \"%dtwo\": zero or negative assets left", + c.of1Unfilled, c.of2Unfilled) + newTests[1].expErr = fmt.Sprintf("cannot fill bid order 1 having assets left \"%done\" with ask "+ + "order 2 having assets left \"%dtwo\": zero or negative assets left", + c.of1Unfilled, c.of2Unfilled) + newTests[2].expErr = fmt.Sprintf("cannot fill ask order 1 having assets left \"%done\" with ask "+ + "order 2 having assets left \"%dtwo\": zero or negative assets left", + c.of1Unfilled, c.of2Unfilled) + newTests[3].expErr = fmt.Sprintf("cannot fill bid order 1 having assets left \"%done\" with bid "+ + "order 2 having assets left \"%dtwo\": zero or negative assets left", + c.of1Unfilled, c.of2Unfilled) + } + tests = append(tests, newTests...) + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if len(tc.expErr) > 0 { + tc.expAmt = sdkmath.ZeroInt() + } + origOF1 := copyOrderFulfillment(tc.of1) + origOF2 := copyOrderFulfillment(tc.of2) + + var amt sdkmath.Int + var err error + testFunc := func() { + amt, err = getFulfillmentAssetsAmt(tc.of1, tc.of2) + } + require.NotPanics(t, testFunc, "getFulfillmentAssetsAmt") + assertions.AssertErrorValue(t, err, tc.expErr, "getFulfillmentAssetsAmt error") + assert.Equal(t, tc.expAmt, amt, "getFulfillmentAssetsAmt amount") + assertEqualOrderFulfillments(t, origOF1, tc.of1, "of1 after getFulfillmentAssetsAmt") + assertEqualOrderFulfillments(t, origOF2, tc.of2, "of2 after getFulfillmentAssetsAmt") + }) + } +} + +func TestSplitPartial(t *testing.T) { + askOrder := func(orderID uint64, assetsAmt int64, seller string) *Order { + return NewOrder(orderID).WithAsk(&AskOrder{ + Seller: seller, + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, + Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(assetsAmt)}, + AllowPartial: true, + }) + } + bidOrder := func(orderID uint64, assetsAmt int64, buyer string) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + Buyer: buyer, + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, + Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(assetsAmt)}, + AllowPartial: true, + }) + } + newOF := func(order *Order, dists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ + Order: order, + AssetsFilledAmt: sdkmath.NewInt(0), + AssetsUnfilledAmt: order.GetAssets().Amount, + PriceAppliedAmt: sdkmath.NewInt(0), + PriceLeftAmt: order.GetPrice().Amount, + } + if len(dists) > 0 { + rv.AssetDists = dists + for _, d := range dists { + rv.AssetsFilledAmt = rv.AssetsFilledAmt.Add(d.Amount) + rv.AssetsUnfilledAmt = rv.AssetsUnfilledAmt.Sub(d.Amount) + } + if rv.AssetsUnfilledAmt.IsZero() { + rv.AssetsUnfilledAmt = sdkmath.NewInt(0) + } + } + return rv + } + dist := func(addr string, amount int64) *distribution { + return &distribution{Address: addr, Amount: sdkmath.NewInt(amount)} + } + + tests := []struct { + name string + askOFs []*orderFulfillment + bidOFs []*orderFulfillment + settlement *Settlement + expAskOFs []*orderFulfillment + expBidOfs []*orderFulfillment + expSettlement *Settlement + expErr string + }{ + { + name: "one ask: not touched", + askOFs: []*orderFulfillment{newOF(askOrder(8, 10, "seller8"))}, + settlement: &Settlement{}, + expErr: "ask order 8 (at index 0) has no assets filled", }, - { - name: "order fees left has negative", - receiver: OrderFulfillment{ - Order: NewOrder(201).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - SellerSettlementFlatFee: coinP(5, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(7, "apple"), Price: coin(89, "plum")}, - }, - OrderFeesLeft: sdk.Coins{coin(2, "fig"), coin(-3, "grape"), coin(4, "honeydew")}, - }, - expErr: "settlement fees left \"2fig,-3grape,4honeydew\" is negative for ask order 201 having fees \"5fig\"", + name: "one ask: partial", + askOFs: []*orderFulfillment{newOF(askOrder(8, 10, "seller8"), dist("buyer", 7))}, + settlement: &Settlement{}, + expAskOFs: []*orderFulfillment{newOF(askOrder(8, 7, "seller8"), dist("buyer", 7))}, + expSettlement: &Settlement{PartialOrderLeft: askOrder(8, 3, "seller8")}, }, { - name: "more fees left than in ask order", - receiver: OrderFulfillment{ - Order: NewOrder(202).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - SellerSettlementFlatFee: coinP(5, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(7, "apple"), Price: coin(89, "plum")}, - }, - OrderFeesLeft: coins(6, "fig"), - }, - expErr: "settlement fees left \"6fig\" is greater than ask order 202 settlement fees \"5fig\"", + name: "one ask: partial, settlement already has a partial", + askOFs: []*orderFulfillment{newOF(askOrder(8, 10, "seller8"), dist("buyer", 7))}, + settlement: &Settlement{PartialOrderLeft: bidOrder(55, 3, "buyer")}, + expErr: "bid order 55 and ask order 8 cannot both be partially filled", }, { - name: "fees left in ask order without fees", - receiver: OrderFulfillment{ - Order: NewOrder(203).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - SellerSettlementFlatFee: nil, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(7, "apple"), Price: coin(89, "plum")}, - }, - OrderFeesLeft: coins(1, "fig"), - }, - expErr: "settlement fees left \"1fig\" is greater than ask order 203 settlement fees \"\"", + name: "one ask: partial, not allowed", + askOFs: []*orderFulfillment{ + newOF(NewOrder(8).WithAsk(&AskOrder{ + Seller: "seller8", + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(10)}, + Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(10)}, + AllowPartial: false, + }), dist("buyer", 7))}, + settlement: &Settlement{}, + expErr: "cannot split ask order 8 having assets \"10apple\" at \"7apple\": order does not allow partial fulfillment", }, { - name: "more fees left than in bid order", - receiver: OrderFulfillment{ - Order: NewOrder(204).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - BuyerSettlementFees: sdk.Coins{coin(5, "fig"), coin(6, "grape")}, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(7, "apple"), Price: coin(89, "plum")}, - }, - OrderFeesLeft: coins(6, "fig"), + name: "two asks: first partial", + askOFs: []*orderFulfillment{ + newOF(askOrder(8, 10, "seller8"), dist("buyer", 7)), + newOF(askOrder(9, 12, "seller8")), }, - expErr: "settlement fees left \"6fig\" is greater than bid order 204 settlement fees \"5fig,6grape\"", + settlement: &Settlement{}, + expErr: "ask order 8 (at index 0) is not filled in full and is not the last ask order provided", }, { - name: "fees left in bid order without fees", - receiver: OrderFulfillment{ - Order: NewOrder(205).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - BuyerSettlementFees: nil, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(45), - AssetsFilledAmt: sdkmath.NewInt(10), - PriceLeftAmt: sdkmath.NewInt(98000), - PriceAppliedAmt: sdkmath.NewInt(789), - PriceUnfilledAmt: sdkmath.NewInt(90000), - PriceFilledAmt: sdkmath.NewInt(8789), - Splits: []*OrderSplit{ - {Assets: coin(3, "apple"), Price: coin(700, "plum")}, - {Assets: coin(7, "apple"), Price: coin(89, "plum")}, - }, - OrderFeesLeft: coins(1, "fig"), + name: "two asks: last untouched", + askOFs: []*orderFulfillment{ + newOF(askOrder(8, 10, "seller8"), dist("buyer", 10)), + newOF(askOrder(9, 12, "seller8")), }, - expErr: "settlement fees left \"1fig\" is greater than bid order 205 settlement fees \"\"", + settlement: &Settlement{}, + expErr: "ask order 9 (at index 1) has no assets filled", }, - { - name: "fully filled, price unfilled positive", - receiver: OrderFulfillment{ - Order: NewOrder(250).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(55), - PriceLeftAmt: sdkmath.NewInt(789), - PriceAppliedAmt: sdkmath.NewInt(98000), - PriceUnfilledAmt: sdkmath.NewInt(788), - PriceFilledAmt: sdkmath.NewInt(98001), - Splits: []*OrderSplit{{Assets: coin(55, "apple"), Price: coin(98000, "plum")}}, - }, - expErr: "fully filled ask order 250 has non-zero unfilled price \"788plum\"", - }, - { - name: "fully filled, order fees left positive", - receiver: OrderFulfillment{ - Order: NewOrder(252).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - SellerSettlementFlatFee: coinP(5, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(55), - PriceLeftAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(98789), - PriceUnfilledAmt: sdkmath.NewInt(0), - PriceFilledAmt: sdkmath.NewInt(98789), - Splits: []*OrderSplit{{Assets: coin(55, "apple"), Price: coin(98789, "plum")}}, - OrderFeesLeft: coins(1, "fig"), + name: "two asks: last partial", + askOFs: []*orderFulfillment{ + newOF(askOrder(8, 10, "seller8"), dist("buyer", 10)), + newOF(askOrder(9, 12, "seller9"), dist("buyer", 10)), + }, + settlement: &Settlement{}, + expAskOFs: []*orderFulfillment{ + newOF(askOrder(8, 10, "seller8"), dist("buyer", 10)), + newOF(askOrder(9, 10, "seller9"), dist("buyer", 10)), }, - expErr: "fully filled ask order 252 has non-zero settlement fees left \"1fig\"", + expSettlement: &Settlement{PartialOrderLeft: askOrder(9, 2, "seller9")}, }, { - name: "ask order, price applied less than filled", - receiver: OrderFulfillment{ - Order: NewOrder(301).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(45), - PriceLeftAmt: sdkmath.NewInt(8789), - PriceAppliedAmt: sdkmath.NewInt(90000), - PriceUnfilledAmt: sdkmath.NewInt(789), - PriceFilledAmt: sdkmath.NewInt(98000), - Splits: []*OrderSplit{{Assets: coin(45, "apple"), Price: coin(90000, "plum")}}, - }, - expErr: "ask order 301 having assets \"55apple\" and price \"98789plum\" cannot be filled by \"45apple\" at price \"90000plum\": insufficient price", - }, - { - name: "ask order, partial, multiple fees left denoms", - receiver: OrderFulfillment{ - Order: NewOrder(302).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - SellerSettlementFlatFee: coinP(3, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(45), - PriceLeftAmt: sdkmath.NewInt(789), - PriceAppliedAmt: sdkmath.NewInt(98000), - PriceUnfilledAmt: sdkmath.NewInt(8789), - PriceFilledAmt: sdkmath.NewInt(90000), - Splits: []*OrderSplit{{Assets: coin(45, "apple"), Price: coin(98000, "plum")}}, - OrderFeesLeft: sdk.Coins{coin(1, "fig"), coin(0, "grape")}, - }, - expErr: "partial fulfillment for ask order 302 having seller settlement fees \"3fig\" has multiple denoms in fees left \"1fig,0grape\"", - }, - { - name: "ask order, tracked fees less than order fees", - receiver: OrderFulfillment{ - Order: NewOrder(303).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - SellerSettlementFlatFee: coinP(123, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(45), - PriceLeftAmt: sdkmath.NewInt(789), - PriceAppliedAmt: sdkmath.NewInt(98000), - PriceUnfilledAmt: sdkmath.NewInt(8789), - PriceFilledAmt: sdkmath.NewInt(90000), - Splits: []*OrderSplit{{Assets: coin(45, "apple"), Price: coin(98000, "plum")}}, - OrderFeesLeft: coins(22, "fig"), - FeesToPay: coins(100, "fig"), - }, - expErr: "tracked settlement fees \"122fig\" is less than ask order 303 settlement fees \"123fig\"", - }, - - { - name: "bid order, price applied less than price filled", - receiver: OrderFulfillment{ - Order: NewOrder(275).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(45), - PriceLeftAmt: sdkmath.NewInt(790), - PriceAppliedAmt: sdkmath.NewInt(97999), - PriceUnfilledAmt: sdkmath.NewInt(789), - PriceFilledAmt: sdkmath.NewInt(98000), - Splits: []*OrderSplit{{Assets: coin(45, "apple"), Price: coin(97999, "plum")}}, - }, - expErr: "price applied \"97999plum\" does not equal price filled \"98000plum\" for bid order 275 having price \"98789plum\"", - }, - { - name: "bid order, price applied more than price filled", - receiver: OrderFulfillment{ - Order: NewOrder(276).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(45), - PriceLeftAmt: sdkmath.NewInt(788), - PriceAppliedAmt: sdkmath.NewInt(98001), - PriceUnfilledAmt: sdkmath.NewInt(789), - PriceFilledAmt: sdkmath.NewInt(98000), - Splits: []*OrderSplit{{Assets: coin(45, "apple"), Price: coin(98001, "plum")}}, - }, - expErr: "price applied \"98001plum\" does not equal price filled \"98000plum\" for bid order 276 having price \"98789plum\"", - }, - { - name: "bid order, tracked fees less than order fees", - receiver: OrderFulfillment{ - Order: NewOrder(277).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - BuyerSettlementFees: coins(123, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(1), - AssetsFilledAmt: sdkmath.NewInt(54), - PriceLeftAmt: sdkmath.NewInt(89), - PriceAppliedAmt: sdkmath.NewInt(98700), - PriceUnfilledAmt: sdkmath.NewInt(89), - PriceFilledAmt: sdkmath.NewInt(98700), - Splits: []*OrderSplit{{Assets: coin(54, "apple"), Price: coin(98700, "plum")}}, - OrderFeesLeft: coins(2, "fig"), - FeesToPay: coins(120, "fig"), - }, - expErr: "tracked settlement fees \"122fig\" does not equal bid order 277 settlement fees \"123fig\"", - }, - { - name: "bid order, tracked fees more than order fees", - receiver: OrderFulfillment{ - Order: NewOrder(277).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - BuyerSettlementFees: coins(123, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(1), - AssetsFilledAmt: sdkmath.NewInt(54), - PriceLeftAmt: sdkmath.NewInt(89), - PriceAppliedAmt: sdkmath.NewInt(98700), - PriceUnfilledAmt: sdkmath.NewInt(89), - PriceFilledAmt: sdkmath.NewInt(98700), - Splits: []*OrderSplit{{Assets: coin(54, "apple"), Price: coin(98700, "plum")}}, - OrderFeesLeft: coins(4, "fig"), - FeesToPay: coins(120, "fig"), - }, - expErr: "tracked settlement fees \"124fig\" does not equal bid order 277 settlement fees \"123fig\"", + name: "one bid: not touched", + bidOFs: []*orderFulfillment{newOF(bidOrder(8, 10, "buyer8"))}, + settlement: &Settlement{}, + expErr: "bid order 8 (at index 0) has no assets filled", }, - { - name: "partial ask, but not allowed", - receiver: OrderFulfillment{ - Order: NewOrder(301).WithAsk(&AskOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), - AllowPartial: false, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(1), - AssetsFilledAmt: sdkmath.NewInt(54), - PriceLeftAmt: sdkmath.NewInt(89), - PriceAppliedAmt: sdkmath.NewInt(98700), - PriceUnfilledAmt: sdkmath.NewInt(89), - PriceFilledAmt: sdkmath.NewInt(98700), - Splits: []*OrderSplit{{Assets: coin(54, "apple"), Price: coin(98700, "plum")}}, - }, - expErr: "cannot fill ask order 301 having assets \"55apple\" with \"54apple\": order does not allow partial fill", + name: "one bid: partial", + bidOFs: []*orderFulfillment{newOF(bidOrder(8, 10, "buyer8"), dist("seller", 7))}, + settlement: &Settlement{}, + expBidOfs: []*orderFulfillment{newOF(bidOrder(8, 7, "buyer8"), dist("seller", 7))}, + expSettlement: &Settlement{PartialOrderLeft: bidOrder(8, 3, "buyer8")}, }, { - name: "partial bid, but not allowed", - receiver: OrderFulfillment{ - Order: NewOrder(302).WithBid(&BidOrder{ - Assets: coin(55, "apple"), - Price: coin(98789, "plum"), + name: "one bid: partial, not allowed", + askOFs: []*orderFulfillment{ + newOF(NewOrder(8).WithBid(&BidOrder{ + Buyer: "buyer8", + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(10)}, + Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(10)}, AllowPartial: false, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(1), - AssetsFilledAmt: sdkmath.NewInt(54), - PriceLeftAmt: sdkmath.NewInt(89), - PriceAppliedAmt: sdkmath.NewInt(98700), - PriceUnfilledAmt: sdkmath.NewInt(89), - PriceFilledAmt: sdkmath.NewInt(98700), - Splits: []*OrderSplit{{Assets: coin(54, "apple"), Price: coin(98700, "plum")}}, - }, - expErr: "cannot fill bid order 302 having assets \"55apple\" with \"54apple\": order does not allow partial fill", + }), dist("seller", 7))}, + settlement: &Settlement{}, + expErr: "cannot split bid order 8 having assets \"10apple\" at \"7apple\": order does not allow partial fulfillment", }, - { - name: "ask, fully filled, exact", - receiver: OrderFulfillment{ - Order: NewOrder(501).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceLeftAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(0), - PriceFilledAmt: sdkmath.NewInt(100), - Splits: []*OrderSplit{{Assets: coin(50, "apple"), Price: coin(100, "plum")}}, + name: "two bids: first partial", + bidOFs: []*orderFulfillment{ + newOF(bidOrder(8, 10, "buyer8"), dist("seller", 7)), + newOF(bidOrder(9, 12, "buyer9")), }, - expErr: "", + settlement: &Settlement{}, + expErr: "bid order 8 (at index 0) is not filled in full and is not the last bid order provided", }, { - name: "ask, fully filled, extra price", - receiver: OrderFulfillment{ - Order: NewOrder(502).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceLeftAmt: sdkmath.NewInt(-5), - PriceAppliedAmt: sdkmath.NewInt(105), - PriceUnfilledAmt: sdkmath.NewInt(0), - PriceFilledAmt: sdkmath.NewInt(100), - Splits: []*OrderSplit{{Assets: coin(50, "apple"), Price: coin(105, "plum")}}, + name: "two bids: last untouched", + bidOFs: []*orderFulfillment{ + newOF(bidOrder(8, 10, "buyer8"), dist("seller", 10)), + newOF(bidOrder(9, 12, "buyer9")), }, - expErr: "", + settlement: &Settlement{}, + expErr: "bid order 9 (at index 1) has no assets filled", }, { - name: "ask, partially filled, exact", - receiver: OrderFulfillment{ - Order: NewOrder(503).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, + name: "two bids: last partial", + bidOFs: []*orderFulfillment{ + newOF(bidOrder(8, 10, "buyer8"), dist("seller", 10)), + newOF(bidOrder(9, 12, "buyer9"), dist("seller", 10)), }, - expErr: "", - }, - { - name: "ask, partially filled, extra price", - receiver: OrderFulfillment{ - Order: NewOrder(504).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(10), - PriceAppliedAmt: sdkmath.NewInt(90), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(90, "plum")}}, + settlement: &Settlement{}, + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(8, 10, "buyer8"), dist("seller", 10)), + newOF(bidOrder(9, 10, "buyer9"), dist("seller", 10)), }, - expErr: "", + expSettlement: &Settlement{PartialOrderLeft: bidOrder(9, 2, "buyer9")}, }, { - name: "bid, fully filled", - receiver: OrderFulfillment{ - Order: NewOrder(505).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceLeftAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(0), - PriceFilledAmt: sdkmath.NewInt(100), - Splits: []*OrderSplit{{Assets: coin(50, "apple"), Price: coin(100, "plum")}}, - }, - expErr: "", + name: "one ask, one bid: both partial", + askOFs: []*orderFulfillment{newOF(askOrder(8, 10, "seller8"), dist("buyer9", 7))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(9, 10, "buyer9"), dist("seller8", 7))}, + settlement: &Settlement{}, + expErr: "ask order 8 and bid order 9 cannot both be partially filled", }, { - name: "bid, partially filled", - receiver: OrderFulfillment{ - Order: NewOrder(506).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, + name: "three asks, three bids: no partial", + askOFs: []*orderFulfillment{ + newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), + newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), + newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), }, - expErr: "", - }, - { - name: "ask, full, no fees, some to pay", - receiver: OrderFulfillment{ - Order: NewOrder(507).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - SellerSettlementFlatFee: nil, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceLeftAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(0), - PriceFilledAmt: sdkmath.NewInt(100), - Splits: []*OrderSplit{{Assets: coin(50, "apple"), Price: coin(100, "plum")}}, - OrderFeesLeft: nil, - FeesToPay: coins(20, "fig"), + bidOFs: []*orderFulfillment{ + newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), + newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), + newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), }, - expErr: "", + settlement: &Settlement{}, + expAskOFs: []*orderFulfillment{ + newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), + newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), + newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), + }, + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), + newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), + newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), + }, + expSettlement: &Settlement{PartialOrderLeft: nil}, }, { - name: "ask, full, with fees, paying exact", - receiver: OrderFulfillment{ - Order: NewOrder(508).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - SellerSettlementFlatFee: coinP(200, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceLeftAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(0), - PriceFilledAmt: sdkmath.NewInt(100), - Splits: []*OrderSplit{{Assets: coin(50, "apple"), Price: coin(100, "plum")}}, - OrderFeesLeft: nil, - FeesToPay: coins(200, "fig"), + name: "three asks, three bids: partial ask", + askOFs: []*orderFulfillment{ + newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), + newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), + newOF(askOrder(12, 21, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), }, - expErr: "", + bidOFs: []*orderFulfillment{ + newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), + newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), + newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), + }, + settlement: &Settlement{}, + expAskOFs: []*orderFulfillment{ + newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), + newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), + newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), + }, + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), + newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), + newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), + }, + expSettlement: &Settlement{PartialOrderLeft: askOrder(12, 1, "seller12")}, }, { - name: "ask, full, with fees, paying more", - receiver: OrderFulfillment{ - Order: NewOrder(509).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - SellerSettlementFlatFee: coinP(200, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(0), - AssetsFilledAmt: sdkmath.NewInt(50), - PriceLeftAmt: sdkmath.NewInt(0), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(0), - PriceFilledAmt: sdkmath.NewInt(100), - Splits: []*OrderSplit{{Assets: coin(50, "apple"), Price: coin(100, "plum")}}, - OrderFeesLeft: nil, - FeesToPay: coins(205, "fig"), + name: "three asks, three bids: no partial", + askOFs: []*orderFulfillment{ + newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), + newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), + newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), }, - expErr: "", + bidOFs: []*orderFulfillment{ + newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), + newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), + newOF(bidOrder(112, 11, "buyer112"), dist("seller12", 10)), + }, + settlement: &Settlement{}, + expAskOFs: []*orderFulfillment{ + newOF(askOrder(51, 10, "seller51"), dist("buyer99", 10)), + newOF(askOrder(77, 15, "seller77"), dist("buyer99", 10), dist("buyer8", 5)), + newOF(askOrder(12, 20, "seller12"), dist("buyer8", 10), dist("buyer112", 10)), + }, + expBidOfs: []*orderFulfillment{ + newOF(bidOrder(99, 20, "buyer99"), dist("seller51", 10), dist("seller77", 10)), + newOF(bidOrder(8, 15, "buyer8"), dist("seller77", 5), dist("seller12", 10)), + newOF(bidOrder(112, 10, "buyer112"), dist("seller12", 10)), + }, + expSettlement: &Settlement{PartialOrderLeft: bidOrder(112, 1, "buyer112")}, }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + origAskOFs := copyOrderFulfillments(tc.askOFs) + origBidOFs := copyOrderFulfillments(tc.bidOFs) + + var err error + testFunc := func() { + err = splitPartial(tc.askOFs, tc.bidOFs, tc.settlement) + } + require.NotPanics(t, testFunc, "splitPartial") + assertions.AssertErrorValue(t, err, tc.expErr, "splitPartial error") + if len(tc.expErr) > 0 { + return + } + if !assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after splitPartial") { + t.Logf("Original: %s", orderFulfillmentsString(origAskOFs)) + } + if !assertEqualOrderFulfillmentSlices(t, tc.expBidOfs, tc.bidOFs, "bidOFs after splitPartial") { + t.Logf("Original: %s", orderFulfillmentsString(origBidOFs)) + } + assert.Equalf(t, tc.expSettlement, tc.settlement, "settlement after splitPartial") + }) + } +} + +func TestSplitOrderFulfillments(t *testing.T) { + acoin := func(amount int64) sdk.Coin { + return sdk.NewInt64Coin("acorn", amount) + } + pcoin := func(amount int64) sdk.Coin { + return sdk.NewInt64Coin("prune", amount) + } + askOrder := func(orderID uint64, assetsAmt int64, allowPartial bool) *Order { + return NewOrder(orderID).WithAsk(&AskOrder{ + MarketId: 123, + Seller: "sEllEr", + Assets: acoin(assetsAmt), + Price: pcoin(assetsAmt), + AllowPartial: allowPartial, + }) + } + bidOrder := func(orderID uint64, assetsAmt int64, allowPartial bool) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + MarketId: 123, + Buyer: "bUyEr", + Assets: acoin(assetsAmt), + Price: pcoin(assetsAmt), + AllowPartial: allowPartial, + }) + } + newOF := func(order *Order, assetsFilledAmt int64) *orderFulfillment { + rv := &orderFulfillment{ + Order: order, + AssetsFilledAmt: sdkmath.NewInt(assetsFilledAmt), + AssetsUnfilledAmt: order.GetAssets().Amount.SubRaw(assetsFilledAmt), + PriceAppliedAmt: sdkmath.NewInt(assetsFilledAmt), + PriceLeftAmt: order.GetPrice().Amount.SubRaw(assetsFilledAmt), + } + // int(x).Sub(x) results in an object that is not .Equal to ZeroInt(). + // The Split function sets this to ZeroInt(). + if rv.AssetsUnfilledAmt.IsZero() { + rv.AssetsUnfilledAmt = sdkmath.ZeroInt() + } + return rv + } + + tests := []struct { + name string + fulfillments []*orderFulfillment + settlement *Settlement + expFulfillments []*orderFulfillment + expSettlement *Settlement + expErr string + }{ { - name: "ask, partial, no fees, some to pay", - receiver: OrderFulfillment{ - Order: NewOrder(510).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - SellerSettlementFlatFee: nil, - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, - OrderFeesLeft: nil, - FeesToPay: coins(20, "fig"), - }, - expErr: "", + name: "one order, ask: nothing filled", + fulfillments: []*orderFulfillment{newOF(askOrder(8, 53, false), 0)}, + settlement: &Settlement{}, + expErr: "ask order 8 (at index 0) has no assets filled", }, { - name: "ask, partial, with fees, paying exact", - receiver: OrderFulfillment{ - Order: NewOrder(511).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - SellerSettlementFlatFee: coinP(20, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, - OrderFeesLeft: nil, - FeesToPay: coins(20, "fig"), - }, - expErr: "", + name: "one order, bid: nothing filled", + fulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, false), 0)}, + settlement: &Settlement{}, + expErr: "bid order 8 (at index 0) has no assets filled", }, { - name: "ask, partial, with fees, paying more", - receiver: OrderFulfillment{ - Order: NewOrder(512).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - SellerSettlementFlatFee: coinP(20, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, - OrderFeesLeft: coins(4, "fig"), - FeesToPay: coins(55, "fig"), - }, - expErr: "", + name: "one order, ask: partially filled", + fulfillments: []*orderFulfillment{newOF(askOrder(8, 53, true), 13)}, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{newOF(askOrder(8, 13, true), 13)}, + expSettlement: &Settlement{PartialOrderLeft: askOrder(8, 40, true)}, }, { - name: "ask, partial, with fees, none being paid", - receiver: OrderFulfillment{ - Order: NewOrder(513).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - SellerSettlementFlatFee: coinP(200, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, - OrderFeesLeft: coins(200, "fig"), - FeesToPay: nil, - }, - expErr: "", + name: "one order, bid: partially filled", + fulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, true), 13)}, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{newOF(bidOrder(8, 13, true), 13)}, + expSettlement: &Settlement{PartialOrderLeft: bidOrder(8, 40, true)}, }, { - name: "bid, partial, with fees, none being paid", - receiver: OrderFulfillment{ - Order: NewOrder(514).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - BuyerSettlementFees: coins(20, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, - OrderFeesLeft: coins(20, "fig"), - FeesToPay: nil, - }, - expErr: "", + name: "one order, ask: partially filled, already have a partially filled", + fulfillments: []*orderFulfillment{newOF(askOrder(8, 53, true), 13)}, + settlement: &Settlement{PartialOrderLeft: bidOrder(951, 357, true)}, + expErr: "bid order 951 and ask order 8 cannot both be partially filled", }, { - name: "bid, partial, with fees, all being paid", - receiver: OrderFulfillment{ - Order: NewOrder(515).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - BuyerSettlementFees: coins(20, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, - OrderFeesLeft: nil, - FeesToPay: coins(20, "fig"), - }, - expErr: "", + name: "one order, bid: partially filled, already have a partially filled", + fulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, true), 13)}, + settlement: &Settlement{PartialOrderLeft: askOrder(951, 357, true)}, + expErr: "ask order 951 and bid order 8 cannot both be partially filled", }, { - name: "bid, partial, with fees, some being paid", - receiver: OrderFulfillment{ - Order: NewOrder(515).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - AllowPartial: true, - BuyerSettlementFees: coins(20, "fig"), - }), - IsFinalized: true, - AssetsUnfilledAmt: sdkmath.NewInt(10), - AssetsFilledAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceUnfilledAmt: sdkmath.NewInt(20), - PriceFilledAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{{Assets: coin(40, "apple"), Price: coin(80, "plum")}}, - OrderFeesLeft: coins(4, "fig"), - FeesToPay: coins(16, "fig"), - }, - expErr: "", + name: "one order, ask: partially filled, split not allowed", + fulfillments: []*orderFulfillment{newOF(askOrder(8, 53, false), 13)}, + settlement: &Settlement{}, + expErr: "cannot split ask order 8 having assets \"53acorn\" at \"13acorn\": order does not allow partial fulfillment", }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - var err error - testFunc := func() { - err = tc.receiver.Validate2() - } - require.NotPanics(t, testFunc, "Validate2") - assertions.AssertErrorValue(t, err, tc.expErr, "Validate2 error") - }) - } -} - -func TestFulfill(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - - tests := []struct { - name string - ofA *OrderFulfillment - ofB *OrderFulfillment - expA *OrderFulfillment - expB *OrderFulfillment - expErr string - swapErr string - }{ { - name: "ask ask", - ofA: &OrderFulfillment{Order: NewOrder(1).WithAsk(&AskOrder{})}, - ofB: &OrderFulfillment{Order: NewOrder(2).WithAsk(&AskOrder{})}, - expErr: "cannot fulfill ask order 1 with ask order 2: order type mismatch", - swapErr: "cannot fulfill ask order 2 with ask order 1: order type mismatch", + name: "one order, bid: partially filled, split not allowed", + fulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, false), 13)}, + settlement: &Settlement{}, + expErr: "cannot split bid order 8 having assets \"53acorn\" at \"13acorn\": order does not allow partial fulfillment", }, { - name: "bid bid", - ofA: &OrderFulfillment{Order: NewOrder(4).WithBid(&BidOrder{})}, - ofB: &OrderFulfillment{Order: NewOrder(3).WithBid(&BidOrder{})}, - expErr: "cannot fulfill bid order 4 with bid order 3: order type mismatch", - swapErr: "cannot fulfill bid order 3 with bid order 4: order type mismatch", + name: "one order, ask: fully filled", + fulfillments: []*orderFulfillment{newOF(askOrder(8, 53, false), 53)}, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{newOF(askOrder(8, 53, false), 53)}, + expSettlement: &Settlement{}, }, { - name: "diff asset denom", - ofA: &OrderFulfillment{Order: NewOrder(5).WithAsk(&AskOrder{Assets: coin(15, "apple")})}, - ofB: &OrderFulfillment{Order: NewOrder(6).WithBid(&BidOrder{Assets: coin(16, "banana")})}, - expErr: "cannot fill bid order 6 having assets \"16banana\" with ask order 5 having assets \"15apple\": denom mismatch", + name: "one order, bid: fully filled", + fulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, false), 53)}, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, false), 53)}, + expSettlement: &Settlement{}, }, { - name: "diff price denom", - ofA: &OrderFulfillment{ - Order: NewOrder(7).WithAsk(&AskOrder{ - Assets: coin(15, "apple"), - Price: coin(17, "pear"), - }), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(8).WithBid(&BidOrder{ - Assets: coin(16, "apple"), - Price: coin(18, "plum"), - }), - }, - expErr: "cannot fill ask order 7 having price \"17pear\" with bid order 8 having price \"18plum\": denom mismatch", + name: "one order, ask: fully filled, already have a partially filled", + fulfillments: []*orderFulfillment{newOF(askOrder(8, 53, false), 53)}, + settlement: &Settlement{PartialOrderLeft: bidOrder(951, 357, true)}, + expFulfillments: []*orderFulfillment{newOF(askOrder(8, 53, false), 53)}, + expSettlement: &Settlement{PartialOrderLeft: bidOrder(951, 357, true)}, }, { - name: "cannot get assets left", - ofA: &OrderFulfillment{ - Order: NewOrder(9).WithAsk(&AskOrder{ - Assets: coin(15, "apple"), - Price: coin(16, "plum"), - }), - AssetsUnfilledAmt: sdkmath.NewInt(-1), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(10).WithBid(&BidOrder{ - Assets: coin(17, "apple"), - Price: coin(18, "plum"), - }), - AssetsUnfilledAmt: sdkmath.NewInt(3), - }, - expErr: "cannot fill ask order 9 having assets left \"-1apple\" with bid order 10 " + - "having assets left \"3apple\": zero or negative assets left", + name: "one order, bid: fully filled, already have a partially filled", + fulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, false), 53)}, + settlement: &Settlement{PartialOrderLeft: askOrder(951, 357, true)}, + expFulfillments: []*orderFulfillment{newOF(bidOrder(8, 53, false), 53)}, + expSettlement: &Settlement{PartialOrderLeft: askOrder(951, 357, true)}, }, { - name: "error from apply", - ofA: &OrderFulfillment{ - Order: NewOrder(11).WithAsk(&AskOrder{ - Assets: coin(15, "apple"), - Price: coin(90, "plum"), - }), - AssetsUnfilledAmt: sdkmath.NewInt(15), - AssetsFilledAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(90), - PriceAppliedAmt: sdkmath.NewInt(0), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(12).WithBid(&BidOrder{ - Assets: coin(30, "apple"), - Price: coin(180, "plum"), - }), - AssetsUnfilledAmt: sdkmath.NewInt(30), - AssetsFilledAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(89), - PriceAppliedAmt: sdkmath.NewInt(91), + name: "three orders, ask: second partially filled", + fulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, true), 16), + newOF(askOrder(10, 200, false), 0), }, - expErr: "cannot fill bid order 12 having price left \"89plum\" to ask order 11 at a price of \"90plum\": overfill", + settlement: &Settlement{}, + expErr: "ask order 9 (at index 1) is not filled in full and is not the last ask order provided", }, { - name: "both filled in full", - ofA: &OrderFulfillment{ - Order: NewOrder(101).WithAsk(&AskOrder{ - Assets: coin(33, "apple"), - Price: coin(57, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(33), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(57), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(102).WithBid(&BidOrder{ - Assets: coin(33, "apple"), - Price: coin(57, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(33), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(57), - }, - expA: &OrderFulfillment{ - Order: NewOrder(101).WithAsk(&AskOrder{ - Assets: coin(33, "apple"), - Price: coin(57, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(33), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(57), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{{Assets: coin(33, "apple"), Price: coin(57, "plum")}}, - }, - expB: &OrderFulfillment{ - Order: NewOrder(102).WithBid(&BidOrder{ - Assets: coin(33, "apple"), - Price: coin(57, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(33), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(57), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{{Assets: coin(33, "apple"), Price: coin(57, "plum")}}, + name: "three orders, bid: second partially filled", + fulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, true), 16), + newOF(bidOrder(10, 200, false), 0), }, + settlement: &Settlement{}, + expErr: "bid order 9 (at index 1) is not filled in full and is not the last bid order provided", }, { - name: "ask, unfilled, gets partially filled", - ofA: &OrderFulfillment{ - Order: NewOrder(103).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(50), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(100), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(104).WithBid(&BidOrder{ - Assets: coin(20, "apple"), - Price: coin(80, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(80), - }, - expA: &OrderFulfillment{ - Order: NewOrder(103).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: sdkmath.NewInt(20), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(80, "plum")}}, - }, - expB: &OrderFulfillment{ - Order: NewOrder(104).WithBid(&BidOrder{ - Assets: coin(20, "apple"), - Price: coin(80, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(80, "plum")}}, + name: "three orders, ask: last not touched", + fulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, false), 17), + newOF(askOrder(10, 200, false), 0), }, + settlement: &Settlement{}, + expErr: "ask order 10 (at index 2) has no assets filled", }, { - name: "ask, partially filled, gets partially filled more", - ofA: &OrderFulfillment{ - Order: NewOrder(105).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(1), - AssetsUnfilledAmt: sdkmath.NewInt(49), - PriceAppliedAmt: sdkmath.NewInt(2), - PriceLeftAmt: sdkmath.NewInt(98), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(88).WithBid(&BidOrder{})}, - Assets: coin(1, "apple"), - Price: coin(2, "plum"), - }, - }, - }, - ofB: &OrderFulfillment{ - Order: NewOrder(106).WithBid(&BidOrder{ - Assets: coin(20, "apple"), - Price: coin(80, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(80), - }, - expA: &OrderFulfillment{ - Order: NewOrder(105).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(21), - AssetsUnfilledAmt: sdkmath.NewInt(29), - PriceAppliedAmt: sdkmath.NewInt(82), - PriceLeftAmt: sdkmath.NewInt(18), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(88).WithBid(&BidOrder{})}, - Assets: coin(1, "apple"), - Price: coin(2, "plum"), - }, - {Assets: coin(20, "apple"), Price: coin(80, "plum")}, - }, - }, - expB: &OrderFulfillment{ - Order: NewOrder(106).WithBid(&BidOrder{ - Assets: coin(20, "apple"), - Price: coin(80, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(80, "plum")}}, + name: "three orders, bid: last not touched", + fulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, false), 17), + newOF(bidOrder(10, 200, true), 0), }, + settlement: &Settlement{}, + expErr: "bid order 10 (at index 2) has no assets filled", }, { - name: "ask, partially filled, gets fully filled", - ofA: &OrderFulfillment{ - Order: NewOrder(107).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(30), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(20), - PriceLeftAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(86).WithBid(&BidOrder{})}, - Assets: coin(30, "apple"), - Price: coin(20, "plum"), - }, - }, - }, - ofB: &OrderFulfillment{ - Order: NewOrder(108).WithBid(&BidOrder{ - Assets: coin(20, "apple"), - Price: coin(80, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(80), - }, - expA: &OrderFulfillment{ - Order: NewOrder(107).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(86).WithBid(&BidOrder{})}, - Assets: coin(30, "apple"), - Price: coin(20, "plum"), - }, - {Assets: coin(20, "apple"), Price: coin(80, "plum")}, - }, + name: "three orders, ask: last partially filled", + fulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, false), 17), + newOF(askOrder(10, 200, true), 183), }, - expB: &OrderFulfillment{ - Order: NewOrder(108).WithBid(&BidOrder{ - Assets: coin(20, "apple"), - Price: coin(80, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(80), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(80, "plum")}}, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, false), 17), + newOF(askOrder(10, 183, true), 183), }, + expSettlement: &Settlement{PartialOrderLeft: askOrder(10, 17, true)}, }, { - name: "bid, unfilled, gets partially filled", - ofA: &OrderFulfillment{ - Order: NewOrder(151).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(30), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(152).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(50), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(100), - }, - expA: &OrderFulfillment{ - Order: NewOrder(151).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(-10), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(40, "plum")}}, - }, - expB: &OrderFulfillment{ - Order: NewOrder(152).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(60), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(40, "plum")}}, + name: "three orders, bid: last partially filled", + fulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, false), 17), + newOF(bidOrder(10, 200, true), 183), + }, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, false), 17), + newOF(bidOrder(10, 183, true), 183), }, + expSettlement: &Settlement{PartialOrderLeft: bidOrder(10, 17, true)}, }, { - name: "bid, unfilled, gets partially filled with truncation", - ofA: &OrderFulfillment{ - Order: NewOrder(153).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(30), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(154).WithBid(&BidOrder{ - Assets: coin(57, "apple"), - Price: coin(331, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(57), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(331), - }, - expA: &OrderFulfillment{ - Order: NewOrder(153).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(116), // 331 * 20 / 57 = 116.140350877193 - PriceLeftAmt: sdkmath.NewInt(-86), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(116, "plum")}}, - }, - expB: &OrderFulfillment{ - Order: NewOrder(154).WithBid(&BidOrder{ - Assets: coin(57, "apple"), - Price: coin(331, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(37), - PriceAppliedAmt: sdkmath.NewInt(116), - PriceLeftAmt: sdkmath.NewInt(215), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(116, "plum")}}, + name: "three orders, ask: last partially filled, split not allowed", + fulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, true), 53), + newOF(askOrder(9, 17, true), 17), + newOF(askOrder(10, 200, false), 183), }, + settlement: &Settlement{}, + expErr: "cannot split ask order 10 having assets \"200acorn\" at \"183acorn\": order does not allow partial fulfillment", }, { - name: "bid, partially filled, gets partially filled more", - ofA: &OrderFulfillment{ - Order: NewOrder(155).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(30), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(156).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(40), - PriceAppliedAmt: sdkmath.NewInt(20), - PriceLeftAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(77).WithAsk(&AskOrder{})}, - Assets: coin(10, "apple"), - Price: coin(20, "plum"), - }, - }, - }, - expA: &OrderFulfillment{ - Order: NewOrder(155).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(-10), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(40, "plum")}}, - }, - expB: &OrderFulfillment{ - Order: NewOrder(156).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(30), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(60), - PriceLeftAmt: sdkmath.NewInt(40), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(77).WithAsk(&AskOrder{})}, - Assets: coin(10, "apple"), - Price: coin(20, "plum"), - }, - {Assets: coin(20, "apple"), Price: coin(40, "plum")}, - }, + name: "three orders, bid: last partially filled, split not allowed", + fulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, true), 53), + newOF(bidOrder(9, 17, true), 17), + newOF(bidOrder(10, 200, false), 183), }, + settlement: &Settlement{}, + expErr: "cannot split bid order 10 having assets \"200acorn\" at \"183acorn\": order does not allow partial fulfillment", }, { - name: "bid, partially filled, gets fully filled", - ofA: &OrderFulfillment{ - Order: NewOrder(157).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(0), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(0), - PriceLeftAmt: sdkmath.NewInt(30), - }, - ofB: &OrderFulfillment{ - Order: NewOrder(158).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(30), - AssetsUnfilledAmt: sdkmath.NewInt(20), - PriceAppliedAmt: sdkmath.NewInt(60), - PriceLeftAmt: sdkmath.NewInt(40), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(75).WithAsk(&AskOrder{})}, - Assets: coin(30, "apple"), - Price: coin(60, "plum"), - }, - }, - }, - expA: &OrderFulfillment{ - Order: NewOrder(157).WithAsk(&AskOrder{ - Assets: coin(20, "apple"), - Price: coin(30, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(-10), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(40, "plum")}}, - }, - expB: &OrderFulfillment{ - Order: NewOrder(158).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(75).WithAsk(&AskOrder{})}, - Assets: coin(30, "apple"), - Price: coin(60, "plum"), - }, - {Assets: coin(20, "apple"), Price: coin(40, "plum")}, - }, + name: "three orders, ask: last partially filled, already have a partially filled", + fulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, true), 53), + newOF(askOrder(9, 17, true), 17), + newOF(askOrder(10, 200, false), 183), }, + settlement: &Settlement{PartialOrderLeft: bidOrder(857, 43, true)}, + expErr: "bid order 857 and ask order 10 cannot both be partially filled", }, { - name: "both partially filled, both get fully filled", - ofA: &OrderFulfillment{ - Order: NewOrder(201).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(60), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1002).WithBid(&BidOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - }, - }, - ofB: &OrderFulfillment{ - Order: NewOrder(202).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(60), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1003).WithAsk(&AskOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - }, - }, - expA: &OrderFulfillment{ - Order: NewOrder(201).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1002).WithBid(&BidOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - {Assets: coin(30, "apple"), Price: coin(60, "plum")}, - }, - }, - expB: &OrderFulfillment{ - Order: NewOrder(202).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1003).WithAsk(&AskOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - {Assets: coin(30, "apple"), Price: coin(60, "plum")}, - }, + name: "three orders, bid: last partially filled, already have a partially filled", + fulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, true), 53), + newOF(bidOrder(9, 17, true), 17), + newOF(bidOrder(10, 200, false), 183), }, + settlement: &Settlement{PartialOrderLeft: askOrder(857, 43, true)}, + expErr: "ask order 857 and bid order 10 cannot both be partially filled", }, { - name: "both partially filled, ask gets fully filled", - ofA: &OrderFulfillment{ - Order: NewOrder(203).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(60), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1004).WithBid(&BidOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - }, + name: "three orders, ask: fully filled", + fulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, false), 17), + newOF(askOrder(10, 200, false), 200), }, - ofB: &OrderFulfillment{ - Order: NewOrder(204).WithBid(&BidOrder{ - Assets: coin(60, "apple"), - Price: coin(120, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(40), - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1005).WithAsk(&AskOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - }, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, false), 17), + newOF(askOrder(10, 200, false), 200), }, - expA: &OrderFulfillment{ - Order: NewOrder(203).WithAsk(&AskOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1004).WithBid(&BidOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - {Assets: coin(30, "apple"), Price: coin(60, "plum")}, - }, + expSettlement: &Settlement{}, + }, + { + name: "three orders, bid: fully filled", + fulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, false), 17), + newOF(bidOrder(10, 200, false), 200), }, - expB: &OrderFulfillment{ - Order: NewOrder(204).WithBid(&BidOrder{ - Assets: coin(60, "apple"), - Price: coin(120, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(10), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(20), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1005).WithAsk(&AskOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - {Assets: coin(30, "apple"), Price: coin(60, "plum")}, - }, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, false), 17), + newOF(bidOrder(10, 200, false), 200), }, + expSettlement: &Settlement{}, }, { - name: "both partially filled, bid gets fully filled", - ofA: &OrderFulfillment{ - Order: NewOrder(205).WithAsk(&AskOrder{ - Assets: coin(60, "apple"), - Price: coin(120, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(40), - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(80), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1006).WithBid(&BidOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - }, + name: "three orders, ask: fully filled, already have a partially filled", + fulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, false), 17), + newOF(askOrder(10, 200, false), 200), }, - ofB: &OrderFulfillment{ - Order: NewOrder(206).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(30), - PriceAppliedAmt: sdkmath.NewInt(40), - PriceLeftAmt: sdkmath.NewInt(60), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1007).WithAsk(&AskOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - }, + settlement: &Settlement{}, + expFulfillments: []*orderFulfillment{ + newOF(askOrder(8, 53, false), 53), + newOF(askOrder(9, 17, false), 17), + newOF(askOrder(10, 200, false), 200), }, - expA: &OrderFulfillment{ - Order: NewOrder(205).WithAsk(&AskOrder{ - Assets: coin(60, "apple"), - Price: coin(120, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: sdkmath.NewInt(10), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(20), - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1006).WithBid(&BidOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - {Assets: coin(30, "apple"), Price: coin(60, "plum")}, - }, + expSettlement: &Settlement{}, + }, + { + name: "three orders, bid: fully filled, already have a partially filled", + fulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, false), 17), + newOF(bidOrder(10, 200, false), 200), }, - expB: &OrderFulfillment{ - Order: NewOrder(206).WithBid(&BidOrder{ - Assets: coin(50, "apple"), - Price: coin(100, "plum"), - }), - AssetsFilledAmt: sdkmath.NewInt(50), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - { - Order: &OrderFulfillment{Order: NewOrder(1007).WithAsk(&AskOrder{})}, - Assets: coin(20, "apple"), - Price: coin(40, "plum"), - }, - {Assets: coin(30, "apple"), Price: coin(60, "plum")}, - }, + settlement: &Settlement{PartialOrderLeft: askOrder(857, 43, true)}, + expFulfillments: []*orderFulfillment{ + newOF(bidOrder(8, 53, false), 53), + newOF(bidOrder(9, 17, false), 17), + newOF(bidOrder(10, 200, false), 200), }, + expSettlement: &Settlement{PartialOrderLeft: askOrder(857, 43, true)}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - if len(tc.expErr) != 0 && len(tc.swapErr) == 0 { - tc.swapErr = tc.expErr - } - if len(tc.expErr) == 0 { - for _, split := range tc.expA.Splits { - if split.Order == nil { - split.Order = tc.expB - } - } - for _, split := range tc.expB.Splits { - if split.Order == nil { - split.Order = tc.expA - } - } - } - - of1, of2 := copyOrderFulfillment(tc.ofA), copyOrderFulfillment(tc.ofB) var err error testFunc := func() { - err = Fulfill(of1, of2) - } - require.NotPanics(t, testFunc, "Fulfill(A, B)") - assertions.AssertErrorValue(t, err, tc.expErr, "Fulfill(A, B) error") - if len(tc.expErr) == 0 { - if !assertEqualOrderFulfillments(t, tc.expA, of1, "Fulfill(A, B): A") { - t.Logf("Original: %s", orderFulfillmentString(tc.ofA)) - } - if !assertEqualOrderFulfillments(t, tc.expB, of2, "Fulfill(A, B): B") { - t.Logf("Original: %s", orderFulfillmentString(tc.ofB)) - } + err = splitOrderFulfillments(tc.fulfillments, tc.settlement) } - - of1, of2 = copyOrderFulfillment(tc.ofB), copyOrderFulfillment(tc.ofA) - require.NotPanics(t, testFunc, "Fulfill(B, A)") - assertions.AssertErrorValue(t, err, tc.swapErr, "Fulfill(B, A) error") - if len(tc.expErr) == 0 { - if !assertEqualOrderFulfillments(t, tc.expB, of1, "Fulfill(B, A): B") { - t.Logf("Original: %s", orderFulfillmentString(tc.ofA)) - } - if !assertEqualOrderFulfillments(t, tc.expA, of2, "Fulfill(B, A): A") { - t.Logf("Original: %s", orderFulfillmentString(tc.ofB)) - } + require.NotPanics(t, testFunc, "splitOrderFulfillments") + assertions.AssertErrorValue(t, err, tc.expErr, "splitOrderFulfillments error") + if len(tc.expErr) > 0 { + return } + assertEqualOrderFulfillmentSlices(t, tc.expFulfillments, tc.fulfillments, "fulfillments after splitOrderFulfillments") + assert.Equal(t, tc.expSettlement, tc.settlement, "settlement after splitOrderFulfillments") }) } } -func TestGetFulfillmentAssetsAmt(t *testing.T) { - newAskOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *OrderFulfillment { - return &OrderFulfillment{ - Order: NewOrder(orderID).WithAsk(&AskOrder{ - Assets: sdk.NewInt64Coin(assetDenom, 999), - }), - AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), - } +func TestAllocatePrice(t *testing.T) { + askOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { + return NewOrder(orderID).WithAsk(&AskOrder{ + MarketId: 123, + Seller: fmt.Sprintf("seller%d", orderID), + Assets: sdk.NewInt64Coin("apple", assetsAmt), + Price: sdk.NewInt64Coin("peach", priceAmt), + }) } - newBidOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *OrderFulfillment { - return &OrderFulfillment{ - Order: NewOrder(orderID).WithBid(&BidOrder{ - Assets: sdk.NewInt64Coin(assetDenom, 999), - }), - AssetsUnfilledAmt: sdkmath.NewInt(assetsUnfilled), - } + bidOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + MarketId: 123, + Buyer: fmt.Sprintf("buyer%d", orderID), + Assets: sdk.NewInt64Coin("apple", assetsAmt), + Price: sdk.NewInt64Coin("peach", priceAmt), + }) } - - cases := []struct { - name string - of1Unfilled int64 - of2Unfilled int64 - expAmt int64 - }{ - {name: "of1 zero", of1Unfilled: 0, of2Unfilled: 3, expAmt: 0}, - {name: "of1 negative", of1Unfilled: -4, of2Unfilled: 3, expAmt: 0}, - {name: "of2 zero", of1Unfilled: 5, of2Unfilled: 0, expAmt: 0}, - {name: "of2 negative", of1Unfilled: 5, of2Unfilled: -6, expAmt: 0}, - {name: "equal", of1Unfilled: 8, of2Unfilled: 8, expAmt: 8}, - {name: "of1 has fewer", of1Unfilled: 9, of2Unfilled: 10, expAmt: 9}, - {name: "of2 has fewer", of1Unfilled: 12, of2Unfilled: 11, expAmt: 11}, + newOF := func(order *Order, dists ...*distribution) *orderFulfillment { + rv := newOrderFulfillment(order) + rv.AssetsFilledAmt, rv.AssetsUnfilledAmt = rv.AssetsUnfilledAmt, rv.AssetsFilledAmt + if len(dists) > 0 { + rv.PriceDists = dists + for _, dist := range dists { + rv.PriceAppliedAmt = rv.PriceAppliedAmt.Add(dist.Amount) + } + rv.PriceLeftAmt = rv.PriceLeftAmt.Sub(rv.PriceAppliedAmt) + } + return rv } - - type testCase struct { - name string - of1 *OrderFulfillment - of2 *OrderFulfillment - expAmt sdkmath.Int - expErr string + dist := func(address string, amount int64) *distribution { + return &distribution{Address: address, Amount: sdkmath.NewInt(amount)} } - tests := make([]testCase, 0, len(cases)*4) - - for _, c := range cases { - newTests := []testCase{ - { - name: "ask bid " + c.name, - of1: newAskOF(1, c.of1Unfilled, "one"), - of2: newBidOF(2, c.of2Unfilled, "two"), - expAmt: sdkmath.NewInt(c.expAmt), + tests := []struct { + name string + askOFs []*orderFulfillment + bidOFs []*orderFulfillment + expAskOFs []*orderFulfillment + expBidOFs []*orderFulfillment + expErr string + }{ + { + name: "total ask price greater than total bid", + askOFs: []*orderFulfillment{ + newOF(askOrder(3, 10, 20)), + newOF(askOrder(4, 10, 20)), + newOF(askOrder(5, 10, 20)), }, - { - name: "bid ask " + c.name, - of1: newBidOF(1, c.of1Unfilled, "one"), - of2: newAskOF(2, c.of2Unfilled, "two"), - expAmt: sdkmath.NewInt(c.expAmt), + bidOFs: []*orderFulfillment{ + newOF(bidOrder(6, 10, 20)), + newOF(bidOrder(7, 10, 19)), + newOF(bidOrder(8, 10, 20)), }, - { - name: "ask ask " + c.name, - of1: newAskOF(1, c.of1Unfilled, "one"), - of2: newAskOF(2, c.of2Unfilled, "two"), - expAmt: sdkmath.NewInt(c.expAmt), + expErr: "total ask price \"60peach\" is greater than total bid price \"59peach\"", + }, + { + name: "one ask, one bid: same price", + askOFs: []*orderFulfillment{newOF(askOrder(3, 10, 60))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(6, 10, 60))}, + expAskOFs: []*orderFulfillment{newOF(askOrder(3, 10, 60), dist("buyer6", 60))}, + expBidOFs: []*orderFulfillment{newOF(bidOrder(6, 10, 60), dist("seller3", 60))}, + }, + { + name: "one ask, one bid: bid more", + askOFs: []*orderFulfillment{newOF(askOrder(3, 10, 60))}, + bidOFs: []*orderFulfillment{newOF(bidOrder(6, 10, 65))}, + expAskOFs: []*orderFulfillment{newOF(askOrder(3, 10, 60), dist("buyer6", 60), dist("buyer6", 5))}, + expBidOFs: []*orderFulfillment{newOF(bidOrder(6, 10, 65), dist("seller3", 60), dist("seller3", 5))}, + }, + { + name: "two asks, two bids: same total price, diff ask prices", + askOFs: []*orderFulfillment{ + newOF(askOrder(3, 10, 21)), + newOF(askOrder(4, 10, 19)), }, - { - name: "bid bid " + c.name, - of1: newBidOF(1, c.of1Unfilled, "one"), - of2: newBidOF(2, c.of2Unfilled, "two"), - expAmt: sdkmath.NewInt(c.expAmt), + bidOFs: []*orderFulfillment{ + newOF(bidOrder(6, 10, 20)), + newOF(bidOrder(7, 10, 20)), }, - } - if c.expAmt == 0 { - newTests[0].expErr = fmt.Sprintf("cannot fill ask order 1 having assets left \"%done\" with bid "+ - "order 2 having assets left \"%dtwo\": zero or negative assets left", - c.of1Unfilled, c.of2Unfilled) - newTests[1].expErr = fmt.Sprintf("cannot fill bid order 1 having assets left \"%done\" with ask "+ - "order 2 having assets left \"%dtwo\": zero or negative assets left", - c.of1Unfilled, c.of2Unfilled) - newTests[2].expErr = fmt.Sprintf("cannot fill ask order 1 having assets left \"%done\" with ask "+ - "order 2 having assets left \"%dtwo\": zero or negative assets left", - c.of1Unfilled, c.of2Unfilled) - newTests[3].expErr = fmt.Sprintf("cannot fill bid order 1 having assets left \"%done\" with bid "+ - "order 2 having assets left \"%dtwo\": zero or negative assets left", - c.of1Unfilled, c.of2Unfilled) - } - tests = append(tests, newTests...) + expAskOFs: []*orderFulfillment{ + newOF(askOrder(3, 10, 21), dist("buyer6", 20), dist("buyer7", 1)), + newOF(askOrder(4, 10, 19), dist("buyer7", 19)), + }, + expBidOFs: []*orderFulfillment{ + newOF(bidOrder(6, 10, 20), dist("seller3", 20)), + newOF(bidOrder(7, 10, 20), dist("seller3", 1), dist("seller4", 19)), + }, + }, + { + name: "three asks, three bids: same total price", + askOFs: []*orderFulfillment{ + newOF(askOrder(3, 10, 25)), + newOF(askOrder(4, 10, 20)), + newOF(askOrder(5, 10, 15)), + }, + bidOFs: []*orderFulfillment{ + newOF(bidOrder(6, 10, 18)), + newOF(bidOrder(7, 10, 30)), + newOF(bidOrder(8, 10, 12)), + }, + expAskOFs: []*orderFulfillment{ + newOF(askOrder(3, 10, 25), dist("buyer6", 18), dist("buyer7", 7)), + newOF(askOrder(4, 10, 20), dist("buyer7", 20)), + newOF(askOrder(5, 10, 15), dist("buyer7", 3), dist("buyer8", 12)), + }, + expBidOFs: []*orderFulfillment{ + newOF(bidOrder(6, 10, 18), dist("seller3", 18)), + newOF(bidOrder(7, 10, 30), dist("seller3", 7), dist("seller4", 20), dist("seller5", 3)), + newOF(bidOrder(8, 10, 12), dist("seller5", 12)), + }, + }, + { + name: "three asks, three bids: bids more", + askOFs: []*orderFulfillment{ + newOF(askOrder(3, 1, 10)), + newOF(askOrder(4, 7, 25)), + newOF(askOrder(5, 22, 30)), + }, + bidOFs: []*orderFulfillment{ + newOF(bidOrder(6, 10, 20)), + newOF(bidOrder(7, 10, 27)), + newOF(bidOrder(8, 10, 30)), + }, + // assets total = 30 + // ask price total = 65 + // bid price total = 77 + // leftover = 12 + expAskOFs: []*orderFulfillment{ + // 12 * 1 / 30 = 0.4 => 0, then 1 + newOF(askOrder(3, 1, 10), dist("buyer6", 10), + dist("buyer8", 1)), + // 12 * 7 / 30 = 2.8 => 2, then because there'll only be 1 left, 1 + newOF(askOrder(4, 7, 25), dist("buyer6", 10), dist("buyer7", 15), + dist("buyer8", 2), dist("buyer8", 1)), + // 12 * 22 / 30 = 8.8 => 8, then nothing because leftovers run out before getting back to it. + newOF(askOrder(5, 22, 30), dist("buyer7", 12), dist("buyer8", 18), + dist("buyer8", 8)), + }, + expBidOFs: []*orderFulfillment{ + newOF(bidOrder(6, 10, 20), dist("seller3", 10), dist("seller4", 10)), + newOF(bidOrder(7, 10, 27), dist("seller4", 15), dist("seller5", 12)), + newOF(bidOrder(8, 10, 30), dist("seller5", 18), dist("seller4", 2), + dist("seller5", 8), dist("seller3", 1), dist("seller4", 1)), + }, + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - if len(tc.expErr) > 0 { - tc.expAmt = sdkmath.ZeroInt() - } - origOF1 := copyOrderFulfillment(tc.of1) - origOF2 := copyOrderFulfillment(tc.of2) - - var amt sdkmath.Int var err error testFunc := func() { - amt, err = GetFulfillmentAssetsAmt(tc.of1, tc.of2) + err = allocatePrice(tc.askOFs, tc.bidOFs) } - require.NotPanics(t, testFunc, "GetFulfillmentAssetsAmt") - assertions.AssertErrorValue(t, err, tc.expErr, "GetFulfillmentAssetsAmt error") - assert.Equal(t, tc.expAmt, amt, "GetFulfillmentAssetsAmt amount") - assertEqualOrderFulfillments(t, origOF1, tc.of1, "of1 after GetFulfillmentAssetsAmt") - assertEqualOrderFulfillments(t, origOF2, tc.of2, "of2 after GetFulfillmentAssetsAmt") + require.NotPanics(t, testFunc, "allocatePrice") + assertions.AssertErrorValue(t, err, tc.expErr, "allocatePrice error") + if len(tc.expErr) > 0 { + return + } + assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after allocatePrice") + assertEqualOrderFulfillmentSlices(t, tc.expBidOFs, tc.bidOFs, "bidOFs after allocatePrice") }) } } func TestGetFulfillmentPriceAmt(t *testing.T) { - newAskOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *OrderFulfillment { - return &OrderFulfillment{ + newAskOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *orderFulfillment { + return &orderFulfillment{ Order: NewOrder(orderID).WithAsk(&AskOrder{ Price: sdk.NewInt64Coin(assetDenom, 999), }), PriceLeftAmt: sdkmath.NewInt(assetsUnfilled), } } - newBidOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *OrderFulfillment { - return &OrderFulfillment{ + newBidOF := func(orderID uint64, assetsUnfilled int64, assetDenom string) *orderFulfillment { + return &orderFulfillment{ Order: NewOrder(orderID).WithBid(&BidOrder{ Price: sdk.NewInt64Coin(assetDenom, 999), }), @@ -8877,8 +4934,8 @@ func TestGetFulfillmentPriceAmt(t *testing.T) { type testCase struct { name string - of1 *OrderFulfillment - of2 *OrderFulfillment + of1 *orderFulfillment + of2 *orderFulfillment expAmt sdkmath.Int expErr string } @@ -8914,2427 +4971,1215 @@ func TestGetFulfillmentPriceAmt(t *testing.T) { } if c.expAmt == 0 { newTests[0].expErr = fmt.Sprintf("cannot fill ask order 1 having price left \"%done\" with bid "+ - "order 2 having price left \"%dtwo\": zero or negative price left", - c.of1Unfilled, c.of2Unfilled) - newTests[1].expErr = fmt.Sprintf("cannot fill bid order 1 having price left \"%done\" with ask "+ - "order 2 having price left \"%dtwo\": zero or negative price left", - c.of1Unfilled, c.of2Unfilled) - newTests[2].expErr = fmt.Sprintf("cannot fill ask order 1 having price left \"%done\" with ask "+ - "order 2 having price left \"%dtwo\": zero or negative price left", - c.of1Unfilled, c.of2Unfilled) - newTests[3].expErr = fmt.Sprintf("cannot fill bid order 1 having price left \"%done\" with bid "+ - "order 2 having price left \"%dtwo\": zero or negative price left", - c.of1Unfilled, c.of2Unfilled) - } - tests = append(tests, newTests...) - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - if len(tc.expErr) > 0 { - tc.expAmt = sdkmath.ZeroInt() - } - origOF1 := copyOrderFulfillment(tc.of1) - origOF2 := copyOrderFulfillment(tc.of2) - - var amt sdkmath.Int - var err error - testFunc := func() { - amt, err = GetFulfillmentPriceAmt(tc.of1, tc.of2) - } - require.NotPanics(t, testFunc, "GetFulfillmentPriceAmt") - assertions.AssertErrorValue(t, err, tc.expErr, "GetFulfillmentPriceAmt error") - assert.Equal(t, tc.expAmt, amt, "GetFulfillmentPriceAmt amount") - assertEqualOrderFulfillments(t, origOF1, tc.of1, "of1 after GetFulfillmentPriceAmt") - assertEqualOrderFulfillments(t, origOF2, tc.of2, "of2 after GetFulfillmentPriceAmt") - }) - } -} - -func TestNewPartialFulfillment(t *testing.T) { - sdkNewInt64CoinP := func(denom string, amt int64) *sdk.Coin { - rv := sdk.NewInt64Coin(denom, amt) - return &rv - } - - tests := []struct { - name string - f *OrderFulfillment - exp *PartialFulfillment - expPanic string - }{ - { - name: "ask order fees left", - f: &OrderFulfillment{ - Order: NewOrder(54).WithAsk(&AskOrder{ - MarketId: 12, - Seller: "the seller", - Assets: sdk.NewInt64Coin("apple", 1234), - Price: sdk.NewInt64Coin("pear", 9876), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 55), - AllowPartial: true, - }), - AssetsFilledAmt: sdkmath.NewInt(234), - AssetsUnfilledAmt: sdkmath.NewInt(1000), - PriceAppliedAmt: sdkmath.NewInt(10000), - PriceLeftAmt: sdkmath.NewInt(-124), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 50)), - PriceFilledAmt: sdkmath.NewInt(876), - PriceUnfilledAmt: sdkmath.NewInt(9000), - }, - exp: &PartialFulfillment{ - NewOrder: NewOrder(54).WithAsk(&AskOrder{ - MarketId: 12, - Seller: "the seller", - Assets: sdk.NewInt64Coin("apple", 1000), - Price: sdk.NewInt64Coin("pear", 9000), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 50), - AllowPartial: true, - }), - AssetsFilled: sdk.NewInt64Coin("apple", 234), - PriceFilled: sdk.NewInt64Coin("pear", 876), - }, - }, - { - name: "ask order no fees left", - f: &OrderFulfillment{ - Order: NewOrder(54).WithAsk(&AskOrder{ - MarketId: 12, - Seller: "the seller", - Assets: sdk.NewInt64Coin("apple", 1234), - Price: sdk.NewInt64Coin("pear", 9876), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 55), - AllowPartial: false, - }), - AssetsFilledAmt: sdkmath.NewInt(234), - AssetsUnfilledAmt: sdkmath.NewInt(1000), - PriceAppliedAmt: sdkmath.NewInt(10000), - PriceLeftAmt: sdkmath.NewInt(-124), - OrderFeesLeft: nil, - PriceFilledAmt: sdkmath.NewInt(876), - PriceUnfilledAmt: sdkmath.NewInt(9000), - }, - exp: &PartialFulfillment{ - NewOrder: NewOrder(54).WithAsk(&AskOrder{ - MarketId: 12, - Seller: "the seller", - Assets: sdk.NewInt64Coin("apple", 1000), - Price: sdk.NewInt64Coin("pear", 9000), - SellerSettlementFlatFee: nil, - AllowPartial: false, - }), - AssetsFilled: sdk.NewInt64Coin("apple", 234), - PriceFilled: sdk.NewInt64Coin("pear", 876), - }, - expPanic: "", - }, - { - name: "ask order multiple fees left", - f: &OrderFulfillment{ - Order: NewOrder(54).WithAsk(&AskOrder{ - MarketId: 12, - Seller: "the seller", - Assets: sdk.NewInt64Coin("apple", 1234), - Price: sdk.NewInt64Coin("pear", 9876), - SellerSettlementFlatFee: sdkNewInt64CoinP("fig", 55), - AllowPartial: true, - }), - AssetsFilledAmt: sdkmath.NewInt(234), - AssetsUnfilledAmt: sdkmath.NewInt(1000), - PriceAppliedAmt: sdkmath.NewInt(10000), - PriceLeftAmt: sdkmath.NewInt(-124), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 50), sdk.NewInt64Coin("grape", 1)), - PriceFilledAmt: sdkmath.NewInt(876), - PriceUnfilledAmt: sdkmath.NewInt(9000), - }, - expPanic: "partially filled ask order 54 somehow has multiple denoms in fees left \"50fig,1grape\"", - }, - { - name: "bid order", - f: &OrderFulfillment{ - Order: NewOrder(54).WithBid(&BidOrder{ - MarketId: 12, - Buyer: "the buyer", - Assets: sdk.NewInt64Coin("apple", 1234), - Price: sdk.NewInt64Coin("pear", 9876), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 55), sdk.NewInt64Coin("grape", 12)), - AllowPartial: true, - }), - AssetsFilledAmt: sdkmath.NewInt(234), - AssetsUnfilledAmt: sdkmath.NewInt(1000), - PriceAppliedAmt: sdkmath.NewInt(9875), - PriceLeftAmt: sdkmath.NewInt(1), - OrderFeesLeft: sdk.NewCoins(sdk.NewInt64Coin("fig", 50)), - PriceFilledAmt: sdkmath.NewInt(876), - PriceUnfilledAmt: sdkmath.NewInt(9000), - }, - exp: &PartialFulfillment{ - NewOrder: NewOrder(54).WithBid(&BidOrder{ - MarketId: 12, - Buyer: "the buyer", - Assets: sdk.NewInt64Coin("apple", 1000), - Price: sdk.NewInt64Coin("pear", 9000), - BuyerSettlementFees: sdk.NewCoins(sdk.NewInt64Coin("fig", 50)), - AllowPartial: true, - }), - AssetsFilled: sdk.NewInt64Coin("apple", 234), - PriceFilled: sdk.NewInt64Coin("pear", 876), - }, - }, - { - name: "nil order type", - f: &OrderFulfillment{ - Order: NewOrder(57), - AssetsFilledAmt: sdkmath.NewInt(5), - PriceFilledAmt: sdkmath.NewInt(6), - }, - expPanic: nilSubTypeErr(57), - }, - { - name: "unknown order type", - f: &OrderFulfillment{ - Order: newUnknownOrder(58), - AssetsFilledAmt: sdkmath.NewInt(5), - PriceFilledAmt: sdkmath.NewInt(6), - }, - expPanic: unknownSubTypeErr(58), - }, - // I don't feel like creating a 3rd order type that implements SubOrderI which would be needed in order to - // have a test case reach the final "order %d has unknown type %q" panic at the end of the func. - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - origF := copyOrderFulfillment(tc.f) - - var actual *PartialFulfillment - testFunc := func() { - actual = NewPartialFulfillment(tc.f) - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "NewPartialFulfillment") - if !assert.Equal(t, tc.exp, actual, "NewPartialFulfillment result") { - t.Logf(" Actual: %s", partialFulfillmentString(actual)) - t.Logf("Expected: %s", partialFulfillmentString(tc.exp)) - } - assertEqualOrderFulfillments(t, origF, tc.f, "OrderFulfillment after NewPartialFulfillment") - }) - } -} - -func TestBuildFulfillments(t *testing.T) { - coin := func(amount int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} - } - coins := func(amount int64, denom string) sdk.Coins { - return sdk.Coins{coin(amount, denom)} - } - - askOrder := func(orderID uint64, assets sdk.Coin, price sdk.Coin, allowPartial bool, fees ...sdk.Coin) *Order { - ao := &AskOrder{ - Seller: "seller", - Assets: assets, - Price: price, - AllowPartial: allowPartial, - } - if len(fees) > 1 { - t.Fatalf("cannot create ask order %d with more than 1 fees %q", orderID, fees) - } - if len(fees) > 0 { - ao.SellerSettlementFlatFee = &fees[0] - } - return NewOrder(orderID).WithAsk(ao) - } - bidOrder := func(orderID uint64, assets sdk.Coin, price sdk.Coin, allowPartial bool, fees ...sdk.Coin) *Order { - return NewOrder(orderID).WithBid(&BidOrder{ - Buyer: "buyer", - Assets: assets, - Price: price, - AllowPartial: allowPartial, - BuyerSettlementFees: fees, - }) - } - filledOF := func(order *Order, priceAmt int64, splits []*OrderSplit, feesToPay ...sdk.Coins) *OrderFulfillment { - rv := &OrderFulfillment{ - Order: order, - AssetsFilledAmt: order.GetAssets().Amount, - AssetsUnfilledAmt: ZeroAmtAfterSub, - Splits: splits, - IsFinalized: true, - PriceFilledAmt: order.GetPrice().Amount, - PriceUnfilledAmt: sdkmath.NewInt(0), - } - if priceAmt != 0 { - rv.PriceAppliedAmt = sdkmath.NewInt(priceAmt) - } else { - rv.PriceAppliedAmt = order.GetPrice().Amount - } - rv.PriceLeftAmt = rv.PriceFilledAmt.Sub(rv.PriceAppliedAmt) - if len(feesToPay) > 0 { - rv.FeesToPay = feesToPay[0] - } - return rv - } - - tests := []struct { - name string - askOrders []*Order - bidOrders []*Order - sellerFeeRatio *FeeRatio - expectedMaker func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments - expErr string - }{ - { - name: "one ask one bid, both fully filled", - askOrders: []*Order{askOrder(5, coin(10, "apple"), coin(55, "prune"), false, coin(8, "fig"))}, - bidOrders: []*Order{bidOrder(6, coin(10, "apple"), coin(60, "prune"), false, coin(33, "fig"))}, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 60, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(60, "prune")}}, // will be BidOFs[0]. - coins(20, "fig"), - ), - }, - BidOFs: []*OrderFulfillment{ - filledOF(bidOrders[0], 0, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(60, "prune")}}, // will be AskOFs[0]. - coins(33, "fig"), - ), - }, - PartialOrder: nil, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - - return rv - }, - }, - { - name: "one ask one bid, ask partially filled", - askOrders: []*Order{askOrder(7, coin(15, "apple"), coin(75, "prune"), true)}, - bidOrders: []*Order{bidOrder(8, coin(10, "apple"), coin(60, "prune"), false)}, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - { - Order: askOrders[0], - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(5), - PriceAppliedAmt: sdkmath.NewInt(60), - PriceLeftAmt: sdkmath.NewInt(15), - Splits: []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(60, "prune")}}, // will be BidOFs[0]. - IsFinalized: true, - FeesToPay: coins(12, "fig"), - OrderFeesLeft: nil, - PriceFilledAmt: sdkmath.NewInt(50), - PriceUnfilledAmt: sdkmath.NewInt(25), - }, - }, - BidOFs: []*OrderFulfillment{ - filledOF(bidOrders[0], 0, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(60, "prune")}}, // will be AskOFs[0]. - ), - }, - PartialOrder: &PartialFulfillment{ - NewOrder: askOrder(7, coin(5, "apple"), coin(25, "prune"), true), - AssetsFilled: coin(10, "apple"), - PriceFilled: coin(50, "prune"), - }, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - - return rv - }, - }, - { - name: "one ask one bid, ask partially filled not allowed", - askOrders: []*Order{askOrder(7, coin(15, "apple"), coin(75, "prune"), false)}, - bidOrders: []*Order{bidOrder(8, coin(10, "apple"), coin(60, "prune"), false)}, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expErr: "cannot fill ask order 7 having assets \"15apple\" with \"10apple\": order does not allow partial fill", - }, - { - name: "one ask one bid, bid partially filled", - askOrders: []*Order{askOrder(9, coin(10, "apple"), coin(50, "prune"), false)}, - bidOrders: []*Order{bidOrder(10, coin(15, "apple"), coin(90, "prune"), true)}, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 60, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(60, "prune")}}, // will be BidOFs[0]. - coins(12, "fig"), - ), - }, - BidOFs: []*OrderFulfillment{ - { - Order: bidOrders[0], - AssetsFilledAmt: sdkmath.NewInt(10), - AssetsUnfilledAmt: sdkmath.NewInt(5), - PriceAppliedAmt: sdkmath.NewInt(60), - PriceLeftAmt: sdkmath.NewInt(30), - Splits: []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(60, "prune")}}, // will be AskOFs[0]. - IsFinalized: true, - FeesToPay: nil, - OrderFeesLeft: nil, - PriceFilledAmt: sdkmath.NewInt(60), - PriceUnfilledAmt: sdkmath.NewInt(30), - }, - }, - PartialOrder: &PartialFulfillment{ - NewOrder: bidOrder(10, coin(5, "apple"), coin(30, "prune"), true), - AssetsFilled: coin(10, "apple"), - PriceFilled: coin(60, "prune"), - }, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - - return rv - }, - }, - { - name: "one ask one bid, bid partially filled not allowed", - askOrders: []*Order{askOrder(9, coin(10, "apple"), coin(50, "prune"), false)}, - bidOrders: []*Order{bidOrder(10, coin(15, "apple"), coin(90, "prune"), false)}, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expErr: "cannot fill bid order 10 having assets \"15apple\" with \"10apple\": order does not allow partial fill", - }, - { - name: "one ask filled by five bids", - askOrders: []*Order{askOrder(21, coin(12, "apple"), coin(60, "prune"), false)}, - bidOrders: []*Order{ - bidOrder(22, coin(1, "apple"), coin(10, "prune"), false), - bidOrder(22, coin(1, "apple"), coin(12, "prune"), false), - bidOrder(22, coin(2, "apple"), coin(1, "prune"), false), - bidOrder(22, coin(3, "apple"), coin(15, "prune"), false), - bidOrder(22, coin(5, "apple"), coin(25, "prune"), false), - }, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 63, - []*OrderSplit{ - {Assets: coin(1, "apple"), Price: coin(10, "prune")}, // Will be BidOFs[0]. - {Assets: coin(1, "apple"), Price: coin(12, "prune")}, // Will be BidOFs[1]. - {Assets: coin(2, "apple"), Price: coin(1, "prune")}, // Will be BidOFs[2]. - {Assets: coin(3, "apple"), Price: coin(15, "prune")}, // Will be BidOFs[3]. - {Assets: coin(5, "apple"), Price: coin(25, "prune")}, // Will be BidOFs[4]. - }, - coins(13, "fig"), - ), - }, - BidOFs: []*OrderFulfillment{ - filledOF(bidOrders[0], 0, - []*OrderSplit{{Assets: coin(1, "apple"), Price: coin(10, "prune")}}, // Will be AskOFs[0]. - ), - filledOF(bidOrders[1], 0, - []*OrderSplit{{Assets: coin(1, "apple"), Price: coin(12, "prune")}}, // Will be AskOFs[0]. - ), - filledOF(bidOrders[2], 0, - []*OrderSplit{{Assets: coin(2, "apple"), Price: coin(1, "prune")}}, // Will be AskOFs[0]. - - ), - filledOF(bidOrders[3], 0, - []*OrderSplit{{Assets: coin(3, "apple"), Price: coin(15, "prune")}}, // Will be AskOFs[0]. - ), - filledOF(bidOrders[4], 0, - []*OrderSplit{{Assets: coin(5, "apple"), Price: coin(25, "prune")}}, // Will be AskOFs[0]. - ), - }, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[0].Splits[1].Order = rv.BidOFs[1] - rv.AskOFs[0].Splits[2].Order = rv.BidOFs[2] - rv.AskOFs[0].Splits[3].Order = rv.BidOFs[3] - rv.AskOFs[0].Splits[4].Order = rv.BidOFs[4] - - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[1].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[2].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[3].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[4].Splits[0].Order = rv.AskOFs[0] - - return rv - }, - }, - { - name: "one indivisible bid filled by five asks", - askOrders: []*Order{ - askOrder(31, coin(1, "apple"), coin(16, "prune"), false), - askOrder(33, coin(1, "apple"), coin(16, "prune"), false), - askOrder(35, coin(1, "apple"), coin(17, "prune"), false), - askOrder(37, coin(13, "apple"), coin(209, "prune"), false), - askOrder(39, coin(15, "apple"), coin(241, "prune"), false), - }, - bidOrders: []*Order{bidOrder(30, coin(31, "apple"), coin(500, "prune"), false)}, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 17, - []*OrderSplit{{Assets: coin(1, "apple"), Price: coin(17, "prune")}}, // Will be BidOfs[0] - coins(4, "fig"), - ), - filledOF(askOrders[1], 0, - []*OrderSplit{{Assets: coin(1, "apple"), Price: coin(16, "prune")}}, // Will be BidOfs[0] - coins(4, "fig"), - ), - filledOF(askOrders[2], 0, - []*OrderSplit{{Assets: coin(1, "apple"), Price: coin(17, "prune")}}, // Will be BidOfs[0] - coins(4, "fig"), - ), - filledOF(askOrders[3], 0, - []*OrderSplit{{Assets: coin(13, "apple"), Price: coin(209, "prune")}}, // Will be BidOfs[0] - coins(42, "fig"), - ), - filledOF(askOrders[4], 0, - []*OrderSplit{{Assets: coin(15, "apple"), Price: coin(241, "prune")}}, // Will be BidOfs[0] - coins(49, "fig"), - ), - }, - BidOFs: []*OrderFulfillment{ - { - Order: bidOrders[0], - AssetsFilledAmt: sdkmath.NewInt(31), - AssetsUnfilledAmt: ZeroAmtAfterSub, - PriceAppliedAmt: sdkmath.NewInt(500), - PriceLeftAmt: ZeroAmtAfterSub, - Splits: []*OrderSplit{ - {Assets: coin(1, "apple"), Price: coin(17, "prune")}, // Will be AskOFs[0] - {Assets: coin(1, "apple"), Price: coin(16, "prune")}, // Will be AskOFs[1] - {Assets: coin(1, "apple"), Price: coin(17, "prune")}, // Will be AskOFs[2] - {Assets: coin(13, "apple"), Price: coin(209, "prune")}, // Will be AskOFs[3] - {Assets: coin(15, "apple"), Price: coin(241, "prune")}, // Will be AskOFs[4] - }, - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(500), - PriceUnfilledAmt: sdkmath.NewInt(0), - }, - }, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[1].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[2].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[3].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[4].Splits[0].Order = rv.BidOFs[0] - - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[0].Splits[1].Order = rv.AskOFs[1] - rv.BidOFs[0].Splits[2].Order = rv.AskOFs[2] - rv.BidOFs[0].Splits[3].Order = rv.AskOFs[3] - rv.BidOFs[0].Splits[4].Order = rv.AskOFs[4] - - return rv - }, - }, - { - name: "three asks three bids, each fully fills the other", - askOrders: []*Order{ - askOrder(51, coin(8, "apple"), coin(55, "prune"), false, coin(18, "grape")), - askOrder(53, coin(12, "apple"), coin(18, "prune"), false, coin(1, "grape")), - askOrder(55, coin(344, "apple"), coin(12345, "prune"), false, coin(99, "grape")), - }, - bidOrders: []*Order{ - bidOrder(52, coin(8, "apple"), coin(55, "prune"), false, coin(3, "fig")), - bidOrder(54, coin(12, "apple"), coin(18, "prune"), false, coin(7, "fig")), - bidOrder(56, coin(344, "apple"), coin(12345, "prune"), false, coin(2, "fig")), - }, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 0, - []*OrderSplit{{Assets: coin(8, "apple"), Price: coin(55, "prune")}}, // Will be BidOFs[0] - coins(18, "grape"), - ), - filledOF(askOrders[1], 0, - []*OrderSplit{{Assets: coin(12, "apple"), Price: coin(18, "prune")}}, // Will be BidOFs[1] - coins(1, "grape"), - ), - filledOF(askOrders[2], 0, - []*OrderSplit{{Assets: coin(344, "apple"), Price: coin(12345, "prune")}}, // Will be BidOFs[2] - coins(99, "grape"), - ), - }, - BidOFs: []*OrderFulfillment{ - filledOF(bidOrders[0], 0, - []*OrderSplit{{Assets: coin(8, "apple"), Price: coin(55, "prune")}}, // Will be AskOFs[0] - coins(3, "fig"), - ), - filledOF(bidOrders[1], 0, - []*OrderSplit{{Assets: coin(12, "apple"), Price: coin(18, "prune")}}, // Will be AskOFs[1] - coins(7, "fig"), - ), - filledOF(bidOrders[2], 0, - []*OrderSplit{{Assets: coin(344, "apple"), Price: coin(12345, "prune")}}, // Will be AskOFs[2] - coins(2, "fig"), - ), - }, - PartialOrder: nil, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[1].Splits[0].Order = rv.BidOFs[1] - rv.AskOFs[2].Splits[0].Order = rv.BidOFs[2] - - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[1].Splits[0].Order = rv.AskOFs[1] - rv.BidOFs[2].Splits[0].Order = rv.AskOFs[2] - - return rv - }, - }, - { - name: "three asks two bids, all fully filled", - askOrders: []*Order{ - askOrder(11, coin(10, "apple"), coin(50, "prune"), false), - askOrder(13, coin(20, "apple"), coin(100, "prune"), false), - askOrder(15, coin(50, "apple"), coin(250, "prune"), false), - }, - bidOrders: []*Order{ - bidOrder(12, coin(23, "apple"), coin(115, "prune"), false), - bidOrder(14, coin(57, "apple"), coin(285, "prune"), false), - }, - sellerFeeRatio: &FeeRatio{Price: coin(5, "prune"), Fee: coin(1, "fig")}, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 0, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(50, "prune")}}, // Will be BidOFs[0] - coins(10, "fig"), - ), - filledOF(askOrders[1], 0, - []*OrderSplit{ - {Assets: coin(13, "apple"), Price: coin(65, "prune")}, // Will be BidOFs[0] - {Assets: coin(7, "apple"), Price: coin(35, "prune")}, // Will be BidOFs[1] - }, - coins(20, "fig"), - ), - filledOF(askOrders[2], 0, - []*OrderSplit{{Assets: coin(50, "apple"), Price: coin(250, "prune")}}, // Will be BidOFs[1] - coins(50, "fig"), - ), - }, - BidOFs: []*OrderFulfillment{ - filledOF(bidOrders[0], 0, - []*OrderSplit{ - {Assets: coin(10, "apple"), Price: coin(50, "prune")}, // Will be AskOfs[0] - {Assets: coin(13, "apple"), Price: coin(65, "prune")}, // Will be AskOfs[1] - }, - ), - filledOF(bidOrders[1], 0, - []*OrderSplit{ - {Assets: coin(7, "apple"), Price: coin(35, "prune")}, // Will be AskOFs[1] - {Assets: coin(50, "apple"), Price: coin(250, "prune")}, // Will be AskOFs[2] - }, - ), - }, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[1].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[1].Splits[1].Order = rv.BidOFs[1] - rv.AskOFs[2].Splits[0].Order = rv.BidOFs[1] - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[0].Splits[1].Order = rv.AskOFs[1] - rv.BidOFs[1].Splits[0].Order = rv.AskOFs[1] - rv.BidOFs[1].Splits[1].Order = rv.AskOFs[2] - - return rv - }, - }, - { - name: "three asks two bids, ask partially filled", - askOrders: []*Order{ - askOrder(73, coin(10, "apple"), coin(50, "prune"), false), - askOrder(75, coin(15, "apple"), coin(75, "prune"), false), - askOrder(77, coin(25, "apple"), coin(125, "prune"), true), - }, - bidOrders: []*Order{ - bidOrder(74, coin(5, "apple"), coin(25, "prune"), false), - bidOrder(76, coin(40, "apple"), coin(200, "prune"), false), - }, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 0, - []*OrderSplit{ - {Assets: coin(5, "apple"), Price: coin(25, "prune")}, // Will be BidOFs[0] - {Assets: coin(5, "apple"), Price: coin(25, "prune")}, // Will be BidOFs[1] - }, - ), - filledOF(askOrders[1], 0, - []*OrderSplit{{Assets: coin(15, "apple"), Price: coin(75, "prune")}}, // Will be BidOFs[1] - ), - { - Order: askOrders[2], - AssetsFilledAmt: sdkmath.NewInt(20), - AssetsUnfilledAmt: sdkmath.NewInt(5), - PriceAppliedAmt: sdkmath.NewInt(100), - PriceLeftAmt: sdkmath.NewInt(25), - Splits: []*OrderSplit{{Assets: coin(20, "apple"), Price: coin(100, "prune")}}, // Will be BidOFs[1], - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(100), - PriceUnfilledAmt: sdkmath.NewInt(25), - }, - }, - BidOFs: []*OrderFulfillment{ - filledOF(bidOrders[0], 0, - []*OrderSplit{{Assets: coin(5, "apple"), Price: coin(25, "prune")}}, // Will be AskOFs[0], - ), - filledOF(bidOrders[1], 0, - []*OrderSplit{ - {Assets: coin(5, "apple"), Price: coin(25, "prune")}, // Will be AskOFs[0], - {Assets: coin(15, "apple"), Price: coin(75, "prune")}, // Will be AskOFs[1], - {Assets: coin(20, "apple"), Price: coin(100, "prune")}, // Will be AskOFs[2], - }, - ), - }, - PartialOrder: &PartialFulfillment{ - NewOrder: askOrder(77, coin(5, "apple"), coin(25, "prune"), true), - AssetsFilled: coin(20, "apple"), - PriceFilled: coin(100, "prune"), - }, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[0].Splits[1].Order = rv.BidOFs[1] - rv.AskOFs[1].Splits[0].Order = rv.BidOFs[1] - rv.AskOFs[2].Splits[0].Order = rv.BidOFs[1] - - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[1].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[1].Splits[1].Order = rv.AskOFs[1] - rv.BidOFs[1].Splits[2].Order = rv.AskOFs[2] - - return rv - }, - }, - { - name: "three asks two bids, ask partially filled not allowed", - askOrders: []*Order{ - askOrder(73, coin(10, "apple"), coin(50, "prune"), false), - askOrder(75, coin(15, "apple"), coin(75, "prune"), false), - askOrder(77, coin(25, "apple"), coin(125, "prune"), false), - }, - bidOrders: []*Order{ - bidOrder(74, coin(5, "apple"), coin(25, "prune"), false), - bidOrder(76, coin(40, "apple"), coin(200, "prune"), false), - }, - expErr: "cannot fill ask order 77 having assets \"25apple\" with \"20apple\": order does not allow partial fill", - }, - { - name: "three asks two bids, bid partially filled", - askOrders: []*Order{ - askOrder(121, coin(55, "apple"), coin(275, "prune"), false), - askOrder(123, coin(12, "apple"), coin(60, "prune"), false), - askOrder(125, coin(13, "apple"), coin(65, "prune"), false), - }, - bidOrders: []*Order{ - bidOrder(124, coin(65, "apple"), coin(325, "prune"), false), - bidOrder(126, coin(20, "apple"), coin(100, "prune"), true), - }, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - rv := &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 0, - []*OrderSplit{{Assets: coin(55, "apple"), Price: coin(275, "prune")}}, // Will be BidOFs[0] - ), - filledOF(askOrders[1], 0, - []*OrderSplit{ - {Assets: coin(10, "apple"), Price: coin(50, "prune")}, // Will be BidOFs[0] - {Assets: coin(2, "apple"), Price: coin(10, "prune")}, // Will be BidOFs[1] - }, - ), - filledOF(askOrders[2], 0, - []*OrderSplit{{Assets: coin(13, "apple"), Price: coin(65, "prune")}}, // Will be BidOFs[1] - ), - }, - BidOFs: []*OrderFulfillment{ - filledOF(bidOrders[0], 0, - []*OrderSplit{ - {Assets: coin(55, "apple"), Price: coin(275, "prune")}, // Will be AskOFs[0] - {Assets: coin(10, "apple"), Price: coin(50, "prune")}, // Will be AskOFs[1] - }, - ), - { - Order: bidOrders[1], - AssetsFilledAmt: sdkmath.NewInt(15), - AssetsUnfilledAmt: sdkmath.NewInt(5), - PriceAppliedAmt: sdkmath.NewInt(75), - PriceLeftAmt: sdkmath.NewInt(25), - Splits: []*OrderSplit{ - {Assets: coin(2, "apple"), Price: coin(10, "prune")}, // Will be AskOFs[1] - {Assets: coin(13, "apple"), Price: coin(65, "prune")}, // Will be AskOFs[2] - }, - IsFinalized: true, - PriceFilledAmt: sdkmath.NewInt(75), - PriceUnfilledAmt: sdkmath.NewInt(25), - }, - }, - PartialOrder: &PartialFulfillment{ - NewOrder: bidOrder(126, coin(5, "apple"), coin(25, "prune"), true), - AssetsFilled: coin(15, "apple"), - PriceFilled: coin(75, "prune"), - }, - } - - rv.AskOFs[0].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[1].Splits[0].Order = rv.BidOFs[0] - rv.AskOFs[1].Splits[1].Order = rv.BidOFs[1] - rv.AskOFs[2].Splits[0].Order = rv.BidOFs[1] - - rv.BidOFs[0].Splits[0].Order = rv.AskOFs[0] - rv.BidOFs[0].Splits[1].Order = rv.AskOFs[1] - rv.BidOFs[1].Splits[0].Order = rv.AskOFs[1] - rv.BidOFs[1].Splits[1].Order = rv.AskOFs[2] - - return rv - }, - }, - { - // TODO[1658]: Either update the process or delete this 2 asks 1 bid unit test. - name: "two asks one bid", - askOrders: []*Order{ - askOrder(91, coin(10, "apple"), coin(49, "prune"), false), - askOrder(93, coin(10, "apple"), coin(51, "prune"), false), - }, - bidOrders: []*Order{bidOrder(92, coin(20, "apple"), coin(100, "prune"), false)}, - expectedMaker: func(t *testing.T, askOrders, bidOrders []*Order) *Fulfillments { - return &Fulfillments{ - AskOFs: []*OrderFulfillment{ - filledOF(askOrders[0], 0, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(49, "prune")}}, // Will be BidOFs[0] - ), - filledOF(askOrders[1], 0, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(51, "prune")}}, // Will be BidOFs[0] - ), - }, - BidOFs: []*OrderFulfillment{ - filledOF(askOrders[1], 0, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(49, "prune")}}, // Will be AskOFs[0] - ), - filledOF(askOrders[1], 0, - []*OrderSplit{{Assets: coin(10, "apple"), Price: coin(51, "prune")}}, // Will be AskOFs[1] - ), - }, - } - }, - }, - { - name: "ask order in bid order list", - askOrders: []*Order{ - askOrder(1, coin(1, "apple"), coin(1, "prune"), false), - bidOrder(2, coin(1, "apple"), coin(1, "prune"), false), - askOrder(3, coin(1, "apple"), coin(1, "prune"), false), - }, - bidOrders: []*Order{bidOrder(4, coin(3, "apple"), coin(3, "prune"), false)}, - expErr: "bid order 2 is not an ask order but is in the askOrders list", - }, - { - name: "bid order in ask order list", - askOrders: []*Order{askOrder(4, coin(3, "apple"), coin(3, "prune"), false)}, - bidOrders: []*Order{ - bidOrder(1, coin(1, "apple"), coin(1, "prune"), false), - askOrder(2, coin(1, "apple"), coin(1, "prune"), false), - bidOrder(3, coin(1, "apple"), coin(1, "prune"), false), - }, - expErr: "ask order 2 is not a bid order but is in the bidOrders list", - }, - // neither filled in full - I'm not sure how I can trigger this. - { - name: "ask finalize error", - askOrders: []*Order{askOrder(15, coin(13, "apple"), coin(17, "prune"), true)}, - bidOrders: []*Order{bidOrder(16, coin(5, "apple"), coin(20, "prune"), true)}, - expErr: "ask order 15 having assets \"13apple\" cannot be partially filled by \"5apple\": price \"17prune\" is not evenly divisible", - }, - { - name: "bid finalize error", - askOrders: []*Order{askOrder(15, coin(5, "apple"), coin(5, "prune"), true)}, - bidOrders: []*Order{bidOrder(16, coin(13, "apple"), coin(17, "prune"), true)}, - expErr: "bid order 16 having assets \"13apple\" cannot be partially filled by \"5apple\": price \"17prune\" is not evenly divisible", - }, - { - name: "validate error", - askOrders: []*Order{askOrder(123, coin(5, "apple"), coin(6, "prune"), true)}, - bidOrders: []*Order{bidOrder(124, coin(5, "apple"), coin(5, "prune"), true)}, - expErr: "ask order 123 having assets \"5apple\" and price \"6prune\" cannot be filled by \"5apple\" at price \"5prune\": insufficient price", - }, - { - name: "nil askOrders", - askOrders: nil, - bidOrders: []*Order{bidOrder(124, coin(5, "apple"), coin(5, "prune"), true)}, - expErr: "no assets filled in bid order 124", - }, - { - name: "empty askOrders", - askOrders: []*Order{}, - bidOrders: []*Order{bidOrder(124, coin(5, "apple"), coin(5, "prune"), true)}, - expErr: "no assets filled in bid order 124", - }, - { - name: "nil bidOrders", - askOrders: []*Order{askOrder(123, coin(5, "apple"), coin(6, "prune"), true)}, - bidOrders: nil, - expErr: "no assets filled in ask order 123", - }, - { - name: "empty bidOrders", - askOrders: []*Order{askOrder(123, coin(5, "apple"), coin(6, "prune"), true)}, - bidOrders: []*Order{}, - expErr: "no assets filled in ask order 123", - }, - { - name: "ask not filled at all", // this gets caught by Finalize. - askOrders: []*Order{ - askOrder(123, coin(10, "apple"), coin(10, "prune"), true), - askOrder(125, coin(5, "apple"), coin(5, "prune"), true), - }, - bidOrders: []*Order{bidOrder(124, coin(5, "apple"), coin(5, "prune"), true)}, - expErr: "no assets filled in ask order 125", - }, - { - name: "bid not filled at all", // this gets caught by Finalize. - askOrders: []*Order{askOrder(123, coin(5, "apple"), coin(5, "prune"), true)}, - bidOrders: []*Order{ - bidOrder(122, coin(10, "apple"), coin(10, "prune"), true), - bidOrder(124, coin(5, "apple"), coin(5, "prune"), true), - }, - expErr: "no assets filled in bid order 124", - }, - // both ask and bid partially filled - I'm not sure how to trigger this. + "order 2 having price left \"%dtwo\": zero or negative price left", + c.of1Unfilled, c.of2Unfilled) + newTests[1].expErr = fmt.Sprintf("cannot fill bid order 1 having price left \"%done\" with ask "+ + "order 2 having price left \"%dtwo\": zero or negative price left", + c.of1Unfilled, c.of2Unfilled) + newTests[2].expErr = fmt.Sprintf("cannot fill ask order 1 having price left \"%done\" with ask "+ + "order 2 having price left \"%dtwo\": zero or negative price left", + c.of1Unfilled, c.of2Unfilled) + newTests[3].expErr = fmt.Sprintf("cannot fill bid order 1 having price left \"%done\" with bid "+ + "order 2 having price left \"%dtwo\": zero or negative price left", + c.of1Unfilled, c.of2Unfilled) + } + tests = append(tests, newTests...) } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var expected *Fulfillments - if tc.expectedMaker != nil { - expected = tc.expectedMaker(t, tc.askOrders, tc.bidOrders) + if len(tc.expErr) > 0 { + tc.expAmt = sdkmath.ZeroInt() } - var actual *Fulfillments + origOF1 := copyOrderFulfillment(tc.of1) + origOF2 := copyOrderFulfillment(tc.of2) + + var amt sdkmath.Int var err error testFunc := func() { - actual, err = BuildFulfillments(tc.askOrders, tc.bidOrders, tc.sellerFeeRatio) - } - require.NotPanics(t, testFunc, "BuildFulfillments") - assertions.AssertErrorValue(t, err, tc.expErr, "BuildFulfillments error") - if !assert.Equal(t, expected, actual, "BuildFulfillments result") && expected != nil && actual != nil { - // Try to help identify the error in the failure logs. - expAskOFs := orderFulfillmentsString(expected.AskOFs) - actAskOFs := orderFulfillmentsString(actual.AskOFs) - if assert.Equal(t, expAskOFs, actAskOFs, "AskOFs") { - // Some difference don't come through in the strings, so dig even deeper. - for i := range expected.AskOFs { - assertEqualOrderFulfillments(t, expected.AskOFs[i], actual.AskOFs[i], "AskOFs[%d]", i) - } - } - expBidOFs := orderFulfillmentsString(expected.BidOFs) - actBidOFs := orderFulfillmentsString(actual.BidOFs) - if assert.Equal(t, expBidOFs, actBidOFs, "BidOFs") { - for i := range expected.BidOFs { - assertEqualOrderFulfillments(t, expected.BidOFs[i], actual.BidOFs[i], "BidOFs[%d]", i) - } - } - expPartial := partialFulfillmentString(expected.PartialOrder) - actPartial := partialFulfillmentString(actual.PartialOrder) - assert.Equal(t, expPartial, actPartial, "PartialOrder") + amt, err = getFulfillmentPriceAmt(tc.of1, tc.of2) } + require.NotPanics(t, testFunc, "getFulfillmentPriceAmt") + assertions.AssertErrorValue(t, err, tc.expErr, "getFulfillmentPriceAmt error") + assert.Equal(t, tc.expAmt, amt, "getFulfillmentPriceAmt amount") + assertEqualOrderFulfillments(t, origOF1, tc.of1, "of1 after getFulfillmentPriceAmt") + assertEqualOrderFulfillments(t, origOF2, tc.of2, "of2 after getFulfillmentPriceAmt") }) } } -// copyIndexedAddrAmts creates a deep copy of an indexedAddrAmts. -func copyIndexedAddrAmts(orig *indexedAddrAmts) *indexedAddrAmts { - if orig == nil { - return nil - } - - rv := &indexedAddrAmts{ - addrs: nil, - amts: nil, - indexes: nil, - } - - if orig.addrs != nil { - rv.addrs = make([]string, 0, len(orig.addrs)) - rv.addrs = append(rv.addrs, orig.addrs...) +func TestSetFeesToPay(t *testing.T) { + coin := func(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} } - - if orig.amts != nil { - rv.amts = make([]sdk.Coins, len(orig.amts)) - for i, amt := range orig.amts { - rv.amts[i] = copyCoins(amt) + askOF := func(orderID uint64, priceAppliedAmt int64, fees ...sdk.Coin) *orderFulfillment { + askOrder := &AskOrder{Price: coin(50, "plum")} + if len(fees) > 1 { + t.Fatalf("cannot provide more than one fee to askOF(%d, %d, %q)", + orderID, priceAppliedAmt, fees) } - } - - if orig.indexes != nil { - rv.indexes = make(map[string]int, len(orig.indexes)) - for k, v := range orig.indexes { - rv.indexes[k] = v + if len(fees) > 0 { + askOrder.SellerSettlementFlatFee = &fees[0] } - } - - return rv -} - -// String converts a indexedAddrAmtsString to a string. -// This is mostly because test failure output of sdk.Coin and sdk.Coins is impossible to understand. -func indexedAddrAmtsString(i *indexedAddrAmts) string { - if i == nil { - return "nil" - } - - addrs := "nil" - if i.addrs != nil { - addrsVals := make([]string, len(i.addrs)) - for j, addr := range i.addrs { - addrsVals[j] = fmt.Sprintf("%q", addr) + return &orderFulfillment{ + Order: NewOrder(orderID).WithAsk(askOrder), + PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), } - addrs = fmt.Sprintf("%T{%s}", i.addrs, strings.Join(addrsVals, ", ")) } - - amts := "nil" - if i.amts != nil { - amtsVals := make([]string, len(i.amts)) - for j, amt := range i.amts { - amtsVals[j] = fmt.Sprintf("%q", amt) + bidOF := func(orderID uint64, priceAppliedAmt int64, fees ...sdk.Coin) *orderFulfillment { + bidOrder := &BidOrder{Price: coin(50, "plum")} + if len(fees) > 0 { + bidOrder.BuyerSettlementFees = fees } - amts = fmt.Sprintf("[]%T{%s}", i.amts, strings.Join(amtsVals, ", ")) - } - - indexes := "nil" - if i.indexes != nil { - indexVals := make([]string, 0, len(i.indexes)) - for k, v := range i.indexes { - indexVals = append(indexVals, fmt.Sprintf("%q: %d", k, v)) + return &orderFulfillment{ + Order: NewOrder(orderID).WithBid(bidOrder), + PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), } - sort.Strings(indexVals) - indexes = fmt.Sprintf("%T{%s}", i.indexes, strings.Join(indexVals, ", ")) - } - - return fmt.Sprintf("%T{addrs:%s, amts:%s, indexes:%s}", i, addrs, amts, indexes) -} - -func TestNewIndexedAddrAmts(t *testing.T) { - expected := &indexedAddrAmts{ - addrs: nil, - amts: nil, - indexes: make(map[string]int), } - actual := newIndexedAddrAmts() - assert.Equal(t, expected, actual, "newIndexedAddrAmts result") - key := "test" - require.NotPanics(t, func() { - _ = actual.indexes[key] - }, "getting value of actual.indexes[%q]", key) -} - -func TestIndexedAddrAmts_Add(t *testing.T) { - coins := func(coins string) sdk.Coins { - rv, err := sdk.ParseCoinsNormalized(coins) - require.NoError(t, err, "sdk.ParseCoinsNormalized(%q)", coins) - return rv + expOF := func(f *orderFulfillment, feesToPay ...sdk.Coin) *orderFulfillment { + if len(feesToPay) > 0 { + f.FeesToPay = sdk.NewCoins(feesToPay...) + } + return f } - negCoins := sdk.Coins{sdk.Coin{Denom: "neg", Amount: sdkmath.NewInt(-1)}} tests := []struct { - name string - receiver *indexedAddrAmts - addr string - coins []sdk.Coin - expected *indexedAddrAmts - expPanic string + name string + askOFs []*orderFulfillment + bidOFs []*orderFulfillment + ratio *FeeRatio + expAskOFs []*orderFulfillment + expBidOFs []*orderFulfillment + expErr string }{ { - name: "empty, add one coin", - receiver: newIndexedAddrAmts(), - addr: "addr1", - coins: coins("1one"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{"addr1": 0}, - }, - }, - { - name: "empty, add two coins", - receiver: newIndexedAddrAmts(), - addr: "addr1", - coins: coins("1one,2two"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one,2two")}, - indexes: map[string]int{"addr1": 0}, - }, - }, - { - name: "empty, add neg coins", - receiver: newIndexedAddrAmts(), - addr: "addr1", - coins: negCoins, - expPanic: "cannot index and add invalid coin amount \"-1neg\"", - }, - { - name: "one addr, add to existing new denom", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{"addr1": 0}, - }, - addr: "addr1", - coins: coins("2two"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one,2two")}, - indexes: map[string]int{"addr1": 0}, + name: "cannot apply ratio", + askOFs: []*orderFulfillment{ + askOF(7777, 55, coin(20, "grape")), + askOF(5555, 71), + askOF(6666, 100), }, - }, - { - name: "one addr, add to existing same denom", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{"addr1": 0}, + bidOFs: []*orderFulfillment{ + bidOF(1111, 100), + bidOF(2222, 200, coin(20, "grape")), + bidOF(3333, 300), }, - addr: "addr1", - coins: coins("3one"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("4one")}, - indexes: map[string]int{"addr1": 0}, + ratio: &FeeRatio{Price: coin(30, "peach"), Fee: coin(1, "fig")}, + expAskOFs: []*orderFulfillment{ + expOF(askOF(7777, 55, coin(20, "grape"))), + expOF(askOF(5555, 71)), + expOF(askOF(6666, 100)), }, - }, - { - name: "one addr, add negative to existing", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{"addr1": 0}, + expBidOFs: []*orderFulfillment{ + expOF(bidOF(1111, 100)), + expOF(bidOF(2222, 200, coin(20, "grape")), coin(20, "grape")), + expOF(bidOF(3333, 300)), }, - addr: "addr1", - coins: negCoins, - expPanic: "cannot index and add invalid coin amount \"-1neg\"", + expErr: joinErrs( + "failed calculate ratio fee for ask order 7777: cannot apply ratio 30peach:1fig to price 55plum: incorrect price denom", + "failed calculate ratio fee for ask order 5555: cannot apply ratio 30peach:1fig to price 71plum: incorrect price denom", + "failed calculate ratio fee for ask order 6666: cannot apply ratio 30peach:1fig to price 100plum: incorrect price denom", + ), }, { - name: "one addr, add to new", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{"addr1": 0}, - }, - addr: "addr2", - coins: coins("2two"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2"}, - amts: []sdk.Coins{coins("1one"), coins("2two")}, - indexes: map[string]int{"addr1": 0, "addr2": 1}, + name: "no ratio", + askOFs: []*orderFulfillment{ + askOF(7777, 55, coin(20, "grape")), + askOF(5555, 71), + askOF(6666, 100), }, - }, - { - name: "one addr, add to new opposite order", - receiver: &indexedAddrAmts{ - addrs: []string{"addr2"}, - amts: []sdk.Coins{coins("2two")}, - indexes: map[string]int{"addr2": 0}, + bidOFs: []*orderFulfillment{ + bidOF(1111, 100), + bidOF(2222, 200, coin(20, "grape")), + bidOF(3333, 300), }, - addr: "addr1", - coins: coins("1one"), - expected: &indexedAddrAmts{ - addrs: []string{"addr2", "addr1"}, - amts: []sdk.Coins{coins("2two"), coins("1one")}, - indexes: map[string]int{"addr2": 0, "addr1": 1}, + ratio: nil, + expAskOFs: []*orderFulfillment{ + expOF(askOF(7777, 55, coin(20, "grape")), coin(20, "grape")), + expOF(askOF(5555, 71)), + expOF(askOF(6666, 100)), }, - }, - { - name: "one addr, add negative to new", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{"addr1": 0}, + expBidOFs: []*orderFulfillment{ + expOF(bidOF(1111, 100)), + expOF(bidOF(2222, 200, coin(20, "grape")), coin(20, "grape")), + expOF(bidOF(3333, 300)), }, - addr: "addr2", - coins: negCoins, - expPanic: "cannot index and add invalid coin amount \"-1neg\"", }, { - name: "three addrs, add to first", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + name: "with ratio", + askOFs: []*orderFulfillment{ + askOF(7777, 55, coin(20, "grape")), + askOF(5555, 71), + askOF(6666, 100), }, - addr: "addr1", - coins: coins("10one"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("11one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + bidOFs: []*orderFulfillment{ + bidOF(1111, 100), + bidOF(2222, 200, coin(20, "grape")), + bidOF(3333, 300), }, - }, - { - name: "three addrs, add to second", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + ratio: &FeeRatio{Price: coin(30, "plum"), Fee: coin(1, "fig")}, + expAskOFs: []*orderFulfillment{ + expOF(askOF(7777, 55, coin(20, "grape")), coin(2, "fig"), coin(20, "grape")), + expOF(askOF(5555, 71), coin(3, "fig")), + expOF(askOF(6666, 100), coin(4, "fig")), }, - addr: "addr2", - coins: coins("10two"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("12two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, + expBidOFs: []*orderFulfillment{ + expOF(bidOF(1111, 100)), + expOF(bidOF(2222, 200, coin(20, "grape")), coin(20, "grape")), + expOF(bidOF(3333, 300)), }, }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var err error + testFunc := func() { + err = setFeesToPay(tc.askOFs, tc.bidOFs, tc.ratio) + } + require.NotPanics(t, testFunc, "setFeesToPay") + assertions.AssertErrorValue(t, err, tc.expErr, "setFeesToPay error") + assertEqualOrderFulfillmentSlices(t, tc.expAskOFs, tc.askOFs, "askOFs after setFeesToPay") + assertEqualOrderFulfillmentSlices(t, tc.expBidOFs, tc.bidOFs, "bidOFs after setFeesToPay") + }) + } +} + +func TestValidateFulfillments(t *testing.T) { + goodAskOF := func(orderID uint64) *orderFulfillment { + return &orderFulfillment{ + Order: NewOrder(orderID).WithAsk(&AskOrder{ + Assets: sdk.NewInt64Coin("apple", 50), + Price: sdk.NewInt64Coin("peach", 123), + }), + AssetsFilledAmt: sdkmath.NewInt(50), + PriceAppliedAmt: sdkmath.NewInt(130), + } + } + badAskOF := func(orderID uint64) *orderFulfillment { + rv := goodAskOF(orderID) + rv.AssetsFilledAmt = sdkmath.NewInt(49) + return rv + } + badAskErr := func(orderID uint64) string { + return badAskOF(orderID).Validate().Error() + } + goodBidOF := func(orderID uint64) *orderFulfillment { + return &orderFulfillment{ + Order: NewOrder(orderID).WithBid(&BidOrder{ + Assets: sdk.NewInt64Coin("apple", 50), + Price: sdk.NewInt64Coin("peach", 123), + }), + AssetsFilledAmt: sdkmath.NewInt(50), + PriceAppliedAmt: sdkmath.NewInt(123), + } + } + badBidOF := func(orderID uint64) *orderFulfillment { + rv := goodBidOF(orderID) + rv.AssetsFilledAmt = sdkmath.NewInt(49) + return rv + } + badBidErr := func(orderID uint64) string { + return badBidOF(orderID).Validate().Error() + } + + tests := []struct { + name string + askOFs []*orderFulfillment + bidOFs []*orderFulfillment + expErr string + }{ { - name: "three addrs, add to third", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, - }, - addr: "addr3", - coins: coins("10three"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("13three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, - }, + name: "all good", + askOFs: []*orderFulfillment{goodAskOF(10), goodAskOF(11), goodAskOF(12)}, + bidOFs: []*orderFulfillment{goodBidOF(20), goodBidOF(21), goodBidOF(22)}, + expErr: "", }, { - name: "three addrs, add two coins to second", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, - }, - addr: "addr2", - coins: coins("10four,20two"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("10four,22two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, - }, + name: "error in one ask", + askOFs: []*orderFulfillment{goodAskOF(10), badAskOF(11), goodAskOF(12)}, + bidOFs: []*orderFulfillment{goodBidOF(20), goodBidOF(21), goodBidOF(22)}, + expErr: badAskErr(11), }, { - name: "three addrs, add to new", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, - }, - addr: "good buddy", - coins: coins("10four"), - expected: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3", "good buddy"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three"), coins("10four")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2, "good buddy": 3}, - }, + name: "error in one bid", + askOFs: []*orderFulfillment{goodAskOF(10), goodAskOF(11), goodAskOF(12)}, + bidOFs: []*orderFulfillment{goodBidOF(20), badBidOF(21), goodBidOF(22)}, + expErr: badBidErr(21), }, { - name: "three addrs, add negative to second", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, - }, - addr: "addr2", - coins: negCoins, - expPanic: "cannot index and add invalid coin amount \"-1neg\"", + name: "two errors in asks", + askOFs: []*orderFulfillment{badAskOF(10), goodAskOF(11), badAskOF(12)}, + bidOFs: []*orderFulfillment{goodBidOF(20), goodBidOF(21), goodBidOF(22)}, + expErr: joinErrs(badAskErr(10), badAskErr(12)), }, { - name: "three addrs, add negative to new", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two"), coins("3three")}, - indexes: map[string]int{"addr1": 0, "addr2": 1, "addr3": 2}, - }, - addr: "addr4", - coins: negCoins, - expPanic: "cannot index and add invalid coin amount \"-1neg\"", + name: "two errors in bids", + askOFs: []*orderFulfillment{goodAskOF(10), goodAskOF(11), goodAskOF(12)}, + bidOFs: []*orderFulfillment{badBidOF(20), goodBidOF(21), badBidOF(22)}, + expErr: joinErrs(badBidErr(20), badBidErr(22)), + }, + { + name: "error in each", + askOFs: []*orderFulfillment{goodAskOF(10), goodAskOF(11), badAskOF(12)}, + bidOFs: []*orderFulfillment{goodBidOF(20), badBidOF(21), goodBidOF(22)}, + expErr: joinErrs(badAskErr(12), badBidErr(21)), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - orig := copyIndexedAddrAmts(tc.receiver) - defer func() { - if t.Failed() { - t.Logf("Original: %s", indexedAddrAmtsString(orig)) - t.Logf(" Actual: %s", indexedAddrAmtsString(tc.receiver)) - t.Logf("Expected: %s", indexedAddrAmtsString(tc.expected)) - } - }() - + var err error testFunc := func() { - tc.receiver.add(tc.addr, tc.coins...) - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "add(%q, %q)", tc.addr, tc.coins) - if len(tc.expPanic) == 0 { - assert.Equal(t, tc.expected, tc.receiver, "receiver after add(%q, %q)", tc.addr, tc.coins) + err = validateFulfillments(tc.askOFs, tc.bidOFs) } + require.NotPanics(t, testFunc, "validateFulfillments") + assertions.AssertErrorValue(t, err, tc.expErr, "validateFulfillments error") }) } } -func TestIndexedAddrAmts_GetAsInputs(t *testing.T) { - coins := func(coins string) sdk.Coins { - rv, err := sdk.ParseCoinsNormalized(coins) - require.NoError(t, err, "sdk.ParseCoinsNormalized(%q)", coins) - return rv +func TestOrderFulfillment_Validate(t *testing.T) { + askOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { + return NewOrder(orderID).WithAsk(&AskOrder{ + MarketId: 987, + Seller: "steve", + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, + Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(priceAmt)}, + }) + } + bidOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { + return NewOrder(orderID).WithBid(&BidOrder{ + MarketId: 987, + Buyer: "bruce", + Assets: sdk.Coin{Denom: "apple", Amount: sdkmath.NewInt(assetsAmt)}, + Price: sdk.Coin{Denom: "peach", Amount: sdkmath.NewInt(priceAmt)}, + }) } tests := []struct { - name string - receiver *indexedAddrAmts - expected []banktypes.Input - expPanic string + name string + f orderFulfillment + expErr string }{ - {name: "nil receiver", receiver: nil, expected: nil}, - {name: "no addrs", receiver: newIndexedAddrAmts(), expected: nil}, { - name: "one addr negative amount", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{{{Denom: "neg", Amount: sdkmath.NewInt(-1)}}}, - indexes: map[string]int{ - "addr1": 0, - }, + name: "nil inside order", + f: orderFulfillment{Order: NewOrder(8)}, + expErr: nilSubTypeErr(8), + }, + { + name: "unknown inside order", + f: orderFulfillment{Order: newUnknownOrder(12)}, + expErr: unknownSubTypeErr(12), + }, + { + name: "order price greater than price applied: ask", + f: orderFulfillment{ + Order: askOrder(52, 10, 401), + PriceAppliedAmt: sdkmath.NewInt(400), + AssetsFilledAmt: sdkmath.NewInt(10), + }, + expErr: "ask order 52 price \"401peach\" is more than price filled \"400peach\"", + }, + { + name: "order price equal to price applied: ask", + f: orderFulfillment{ + Order: askOrder(53, 10, 401), + PriceAppliedAmt: sdkmath.NewInt(401), + AssetsFilledAmt: sdkmath.NewInt(10), + }, + expErr: "", + }, + { + name: "order price less than price applied: ask", + f: orderFulfillment{ + Order: askOrder(54, 10, 401), + PriceAppliedAmt: sdkmath.NewInt(402), + AssetsFilledAmt: sdkmath.NewInt(10), }, - expPanic: "invalid indexed amount \"addr1\" for address \"-1neg\": cannot be zero or negative", + expErr: "", }, { - name: "one addr zero amount", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{{{Denom: "zero", Amount: sdkmath.NewInt(0)}}}, - indexes: map[string]int{ - "addr1": 0, - }, + name: "order price greater than price applied: bid", + f: orderFulfillment{ + Order: bidOrder(71, 17, 432), + PriceAppliedAmt: sdkmath.NewInt(431), + AssetsFilledAmt: sdkmath.NewInt(17), }, - expPanic: "invalid indexed amount \"addr1\" for address \"0zero\": cannot be zero or negative", + expErr: "bid order 71 price \"432peach\" is not equal to price filled \"431peach\"", }, { - name: "one addr positive amount", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{ - "addr1": 0, - }, + name: "order price equal to price applied: bid", + f: orderFulfillment{ + Order: bidOrder(72, 17, 432), + PriceAppliedAmt: sdkmath.NewInt(432), + AssetsFilledAmt: sdkmath.NewInt(17), }, - expected: []banktypes.Input{ - {Address: "addr1", Coins: coins("1one")}, + expErr: "", + }, + { + name: "order price less than price applied: bid", + f: orderFulfillment{ + Order: bidOrder(73, 17, 432), + PriceAppliedAmt: sdkmath.NewInt(433), + AssetsFilledAmt: sdkmath.NewInt(17), }, + expErr: "bid order 73 price \"432peach\" is not equal to price filled \"433peach\"", }, { - name: "two addrs", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2"}, - amts: []sdk.Coins{coins("1one"), coins("2two,3three")}, - indexes: map[string]int{ - "addr1": 0, - "addr2": 1, - }, + name: "order assets less than assets filled: ask", + f: orderFulfillment{ + Order: askOrder(101, 53, 12345), + PriceAppliedAmt: sdkmath.NewInt(12345), + AssetsFilledAmt: sdkmath.NewInt(54), }, - expected: []banktypes.Input{ - {Address: "addr1", Coins: coins("1one")}, - {Address: "addr2", Coins: coins("2two,3three")}, + expErr: "ask order 101 assets \"53apple\" does not equal filled assets \"54apple\"", + }, + { + name: "order assets equal to assets filled: ask", + f: orderFulfillment{ + Order: askOrder(202, 53, 12345), + PriceAppliedAmt: sdkmath.NewInt(12345), + AssetsFilledAmt: sdkmath.NewInt(53), }, + expErr: "", }, { - name: "three addrs", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two,3three"), coins("4four,5five,6six")}, - indexes: map[string]int{ - "addr1": 0, - "addr2": 1, - "addr3": 2, - }, + name: "order assets more than assets filled: ask", + f: orderFulfillment{ + Order: askOrder(303, 53, 12345), + PriceAppliedAmt: sdkmath.NewInt(12345), + AssetsFilledAmt: sdkmath.NewInt(52), }, - expected: []banktypes.Input{ - {Address: "addr1", Coins: coins("1one")}, - {Address: "addr2", Coins: coins("2two,3three")}, - {Address: "addr3", Coins: coins("4four,5five,6six")}, + expErr: "ask order 303 assets \"53apple\" does not equal filled assets \"52apple\"", + }, + { + name: "order assets less than assets filled: bid", + f: orderFulfillment{ + Order: bidOrder(404, 53, 12345), + PriceAppliedAmt: sdkmath.NewInt(12345), + AssetsFilledAmt: sdkmath.NewInt(54), }, + expErr: "bid order 404 assets \"53apple\" does not equal filled assets \"54apple\"", }, { - name: "three addrs, negative in third", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{ - coins("1one"), - coins("2two,3three"), - { - {Denom: "acoin", Amount: sdkmath.NewInt(4)}, - {Denom: "bcoin", Amount: sdkmath.NewInt(5)}, - {Denom: "ncoin", Amount: sdkmath.NewInt(-6)}, - {Denom: "zcoin", Amount: sdkmath.NewInt(7)}, - }, - }, - indexes: map[string]int{ - "addr1": 0, - "addr2": 1, - "addr3": 2, - }, + name: "order assets equal to assets filled: bid", + f: orderFulfillment{ + Order: bidOrder(505, 53, 12345), + PriceAppliedAmt: sdkmath.NewInt(12345), + AssetsFilledAmt: sdkmath.NewInt(53), }, - expPanic: "invalid indexed amount \"addr3\" for address \"4acoin,5bcoin,-6ncoin,7zcoin\": cannot be zero or negative", + expErr: "", + }, + { + name: "order assets more than assets filled: bid", + f: orderFulfillment{ + Order: bidOrder(606, 53, 12345), + PriceAppliedAmt: sdkmath.NewInt(12345), + AssetsFilledAmt: sdkmath.NewInt(52), + }, + expErr: "bid order 606 assets \"53apple\" does not equal filled assets \"52apple\"", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - orig := copyIndexedAddrAmts(tc.receiver) - var actual []banktypes.Input + var err error testFunc := func() { - actual = tc.receiver.getAsInputs() - } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getAsInputs()") - assert.Equal(t, tc.expected, actual, "getAsInputs() result") - if !assert.Equal(t, orig, tc.receiver, "receiver before and after getAsInputs()") { - t.Logf("Before: %s", indexedAddrAmtsString(orig)) - t.Logf(" After: %s", indexedAddrAmtsString(tc.receiver)) + err = tc.f.Validate() } + require.NotPanics(t, testFunc, "Validate") + assertions.AssertErrorValue(t, err, tc.expErr, "Validate error") }) } } -func TestIndexedAddrAmts_GetAsOutputs(t *testing.T) { - coins := func(coins string) sdk.Coins { - rv, err := sdk.ParseCoinsNormalized(coins) - require.NoError(t, err, "sdk.ParseCoinsNormalized(%q)", coins) - return rv - } - +func TestBuildTransfers(t *testing.T) { tests := []struct { - name string - receiver *indexedAddrAmts - expected []banktypes.Output - expPanic string + name string + askOFs []*orderFulfillment + bidOFs []*orderFulfillment + expSettlement *Settlement + expErr string }{ - {name: "nil receiver", receiver: nil, expected: nil}, - {name: "no addrs", receiver: newIndexedAddrAmts(), expected: nil}, { - name: "one addr negative amount", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{{{Denom: "neg", Amount: sdkmath.NewInt(-1)}}}, - indexes: map[string]int{ - "addr1": 0, + name: "ask with negative assets filled", + askOFs: []*orderFulfillment{ + { + Order: NewOrder(18).WithAsk(&AskOrder{ + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 42), + }), + AssetsFilledAmt: sdkmath.NewInt(-1), + PriceAppliedAmt: sdkmath.NewInt(-1), }, }, - expPanic: "invalid indexed amount \"addr1\" for address \"-1neg\": cannot be zero or negative", + expErr: "ask order 18 cannot be filled with \"-1apple\" assets: amount not positive", }, { - name: "one addr zero amount", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{{{Denom: "zero", Amount: sdkmath.NewInt(0)}}}, - indexes: map[string]int{ - "addr1": 0, + name: "bid with negative assets filled", + bidOFs: []*orderFulfillment{ + { + Order: NewOrder(12).WithBid(&BidOrder{ + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 42), + }), + AssetsFilledAmt: sdkmath.NewInt(-1), + PriceAppliedAmt: sdkmath.NewInt(-1), }, }, - expPanic: "invalid indexed amount \"addr1\" for address \"0zero\": cannot be zero or negative", + expErr: "bid order 12 cannot be filled at price \"-1plum\": amount not positive", }, { - name: "one addr positive amount", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1"}, - amts: []sdk.Coins{coins("1one")}, - indexes: map[string]int{ - "addr1": 0, + name: "ask with negative fees to pay", + askOFs: []*orderFulfillment{ + { + Order: NewOrder(53).WithAsk(&AskOrder{ + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 42), + }), + AssetsFilledAmt: sdkmath.NewInt(15), + AssetDists: []*distribution{{Address: "buyer1", Amount: sdkmath.NewInt(15)}}, + PriceAppliedAmt: sdkmath.NewInt(42), + PriceDists: []*distribution{{Address: "seller1", Amount: sdkmath.NewInt(42)}}, + FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(-1)}}, }, }, - expected: []banktypes.Output{ - {Address: "addr1", Coins: coins("1one")}, - }, + expErr: "ask order 53 cannot pay \"-1fig\" in fees: negative amount", }, { - name: "two addrs", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2"}, - amts: []sdk.Coins{coins("1one"), coins("2two,3three")}, - indexes: map[string]int{ - "addr1": 0, - "addr2": 1, + name: "bid with negative fees to pay", + bidOFs: []*orderFulfillment{ + { + Order: NewOrder(35).WithBid(&BidOrder{ + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 42), + }), + AssetsFilledAmt: sdkmath.NewInt(15), + AssetDists: []*distribution{{Address: "seller1", Amount: sdkmath.NewInt(15)}}, + PriceAppliedAmt: sdkmath.NewInt(42), + PriceDists: []*distribution{{Address: "seller1", Amount: sdkmath.NewInt(42)}}, + FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(-1)}}, }, }, - expected: []banktypes.Output{ - {Address: "addr1", Coins: coins("1one")}, - {Address: "addr2", Coins: coins("2two,3three")}, - }, + expErr: "bid order 35 cannot pay \"-1fig\" in fees: negative amount", }, { - name: "three addrs", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{coins("1one"), coins("2two,3three"), coins("4four,5five,6six")}, - indexes: map[string]int{ - "addr1": 0, - "addr2": 1, - "addr3": 2, + name: "two asks, three bids", + askOFs: []*orderFulfillment{ + { + Order: NewOrder(77).WithAsk(&AskOrder{ + Seller: "seller77", + Assets: sdk.NewInt64Coin("apple", 15), + Price: sdk.NewInt64Coin("plum", 42), + }), + AssetsFilledAmt: sdkmath.NewInt(15), + AssetDists: []*distribution{ + {Address: "buyer5511", Amount: sdkmath.NewInt(15)}, + }, + PriceAppliedAmt: sdkmath.NewInt(43), + PriceDists: []*distribution{ + {Address: "buyer5511", Amount: sdkmath.NewInt(30)}, + {Address: "buyer78", Amount: sdkmath.NewInt(12)}, + {Address: "buyer9001", Amount: sdkmath.NewInt(1)}, + }, + FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(11)}}, + }, + { + Order: NewOrder(3).WithAsk(&AskOrder{ + Seller: "seller3", + Assets: sdk.NewInt64Coin("apple", 43), + Price: sdk.NewInt64Coin("plum", 88), + }), + AssetsFilledAmt: sdkmath.NewInt(43), + AssetDists: []*distribution{ + {Address: "buyer5511", Amount: sdkmath.NewInt(5)}, + {Address: "buyer78", Amount: sdkmath.NewInt(7)}, + {Address: "buyer9001", Amount: sdkmath.NewInt(31)}, + }, + PriceAppliedAmt: sdkmath.NewInt(90), + PriceDists: []*distribution{ + {Address: "buyer78", Amount: sdkmath.NewInt(5)}, + {Address: "buyer9001", Amount: sdkmath.NewInt(83)}, + {Address: "buyer9001", Amount: sdkmath.NewInt(2)}, + }, + FeesToPay: nil, }, }, - expected: []banktypes.Output{ - {Address: "addr1", Coins: coins("1one")}, - {Address: "addr2", Coins: coins("2two,3three")}, - {Address: "addr3", Coins: coins("4four,5five,6six")}, + bidOFs: []*orderFulfillment{ + { + Order: NewOrder(5511).WithBid(&BidOrder{ + Buyer: "buyer5511", + Assets: sdk.NewInt64Coin("apple", 20), + Price: sdk.NewInt64Coin("plum", 30), + }), + AssetsFilledAmt: sdkmath.NewInt(20), + AssetDists: []*distribution{ + {Address: "seller77", Amount: sdkmath.NewInt(15)}, + {Address: "seller3", Amount: sdkmath.NewInt(5)}, + }, + PriceAppliedAmt: sdkmath.NewInt(30), + PriceDists: []*distribution{ + {Address: "seller77", Amount: sdkmath.NewInt(30)}, + }, + FeesToPay: sdk.Coins{sdk.Coin{Denom: "fig", Amount: sdkmath.NewInt(10)}}, + }, + { + Order: NewOrder(78).WithBid(&BidOrder{ + Buyer: "buyer78", + Assets: sdk.NewInt64Coin("apple", 7), + Price: sdk.NewInt64Coin("plum", 15), + }), + AssetsFilledAmt: sdkmath.NewInt(7), + AssetDists: []*distribution{ + {Address: "seller3", Amount: sdkmath.NewInt(7)}, + }, + PriceAppliedAmt: sdkmath.NewInt(15), + PriceDists: []*distribution{ + {Address: "seller77", Amount: sdkmath.NewInt(12)}, + {Address: "seller3", Amount: sdkmath.NewInt(3)}, + }, + FeesToPay: sdk.Coins{sdk.Coin{Denom: "grape", Amount: sdkmath.NewInt(4)}}, + }, + { + Order: NewOrder(9001).WithBid(&BidOrder{ + Buyer: "buyer9001", + Assets: sdk.NewInt64Coin("apple", 31), + Price: sdk.NewInt64Coin("plum", 86), + }), + AssetsFilledAmt: sdkmath.NewInt(31), + AssetDists: []*distribution{ + {Address: "seller3", Amount: sdkmath.NewInt(31)}, + }, + PriceAppliedAmt: sdkmath.NewInt(86), + PriceDists: []*distribution{ + {Address: "seller3", Amount: sdkmath.NewInt(83)}, + {Address: "seller77", Amount: sdkmath.NewInt(2)}, + {Address: "seller3", Amount: sdkmath.NewInt(1)}, + }, + FeesToPay: nil, + }, }, - }, - { - name: "three addrs, negative in third", - receiver: &indexedAddrAmts{ - addrs: []string{"addr1", "addr2", "addr3"}, - amts: []sdk.Coins{ - coins("1one"), - coins("2two,3three"), + expSettlement: &Settlement{ + Transfers: []*Transfer{ { - {Denom: "acoin", Amount: sdkmath.NewInt(4)}, - {Denom: "bcoin", Amount: sdkmath.NewInt(5)}, - {Denom: "ncoin", Amount: sdkmath.NewInt(-6)}, - {Denom: "zcoin", Amount: sdkmath.NewInt(7)}, + Inputs: []banktypes.Input{{Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 15))}}, + Outputs: []banktypes.Output{{Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 15))}}, + }, + { + Inputs: []banktypes.Input{{Address: "seller3", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 43))}}, + Outputs: []banktypes.Output{ + {Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 5))}, + {Address: "buyer78", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 7))}, + {Address: "buyer9001", Coins: sdk.NewCoins(sdk.NewInt64Coin("apple", 31))}, + }, + }, + { + Inputs: []banktypes.Input{{Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 30))}}, + Outputs: []banktypes.Output{{Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 30))}}, + }, + { + Inputs: []banktypes.Input{{Address: "buyer78", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 15))}}, + Outputs: []banktypes.Output{ + {Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 12))}, + {Address: "seller3", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 3))}, + }, + }, + { + Inputs: []banktypes.Input{{Address: "buyer9001", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 86))}}, + Outputs: []banktypes.Output{ + {Address: "seller3", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 84))}, + {Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("plum", 2))}, + }, }, }, - indexes: map[string]int{ - "addr1": 0, - "addr2": 1, - "addr3": 2, + FeeInputs: []banktypes.Input{ + {Address: "seller77", Coins: sdk.NewCoins(sdk.NewInt64Coin("fig", 11))}, + {Address: "buyer5511", Coins: sdk.NewCoins(sdk.NewInt64Coin("fig", 10))}, + {Address: "buyer78", Coins: sdk.NewCoins(sdk.NewInt64Coin("grape", 4))}, }, }, - expPanic: "invalid indexed amount \"addr3\" for address \"4acoin,5bcoin,-6ncoin,7zcoin\": cannot be zero or negative", + expErr: "", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - orig := copyIndexedAddrAmts(tc.receiver) - var actual []banktypes.Output + settlement := &Settlement{} + if tc.expSettlement == nil { + tc.expSettlement = &Settlement{} + } + var err error testFunc := func() { - actual = tc.receiver.getAsOutputs() + err = buildTransfers(tc.askOFs, tc.bidOFs, settlement) } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getAsOutputs()") - assert.Equal(t, tc.expected, actual, "getAsOutputs() result") - if !assert.Equal(t, orig, tc.receiver, "receiver before and after getAsInputs()") { - t.Logf("Before: %s", indexedAddrAmtsString(orig)) - t.Logf(" After: %s", indexedAddrAmtsString(tc.receiver)) + require.NotPanics(t, testFunc, "buildTransfers") + assertions.AssertErrorValue(t, err, tc.expErr, "buildTransfers error") + if !assert.Equal(t, tc.expSettlement, settlement, "settlement after buildTransfers") { + expTransStrs := make([]string, len(tc.expSettlement.Transfers)) + for i, t := range tc.expSettlement.Transfers { + expTransStrs[i] = fmt.Sprintf("[%d]%s", i, transferString(t)) + } + expTrans := strings.Join(expTransStrs, "\n") + actTransStrs := make([]string, len(tc.expSettlement.Transfers)) + for i, t := range settlement.Transfers { + actTransStrs[i] = fmt.Sprintf("[%d]%s", i, transferString(t)) + } + actTrans := strings.Join(actTransStrs, "\n") + assert.Equal(t, expTrans, actTrans, "transfers (as strings)") } }) } } -// fulfillmentsString is similar to %v except with easier to understand Coin entries. -func fulfillmentsString(f *Fulfillments) string { - if f == nil { - return "nil" - } - - fields := []string{ - fmt.Sprintf("AskOFs: %s", orderFulfillmentsString(f.AskOFs)), - fmt.Sprintf("BidOFs: %s", orderFulfillmentsString(f.BidOFs)), - fmt.Sprintf("PartialOrder: %s", partialFulfillmentString(f.PartialOrder)), - } - return fmt.Sprintf("{%s}", strings.Join(fields, ", ")) -} - -// orderFulfillmentsString is similar to %v except with easier to understand Coin entries. -func orderFulfillmentsString(ofs []*OrderFulfillment) string { - if ofs == nil { - return "nil" - } - - vals := make([]string, len(ofs)) - for i, f := range ofs { - vals[i] = orderFulfillmentString(f) - } - return fmt.Sprintf("[%s]", strings.Join(vals, ", ")) -} - -// partialFulfillmentString is similar to %v except with easier to understand Coin entries. -func partialFulfillmentString(p *PartialFulfillment) string { - if p == nil { - return "nil" - } - - fields := []string{ - fmt.Sprintf("NewOrder:%s", orderString(p.NewOrder)), - fmt.Sprintf("AssetsFilled:%s", coinPString(&p.AssetsFilled)), - fmt.Sprintf("PriceFilled:%s", coinPString(&p.PriceFilled)), - } - return fmt.Sprintf("{%s}", strings.Join(fields, ", ")) -} - -// transferString is similar to %v except with easier to understand Coin entries. -func settlementTransfersString(s *SettlementTransfers) string { - if s == nil { - return "nil" - } - - orderTransfers := "nil" - if s.OrderTransfers != nil { - transVals := make([]string, len(s.OrderTransfers)) - for i, trans := range s.OrderTransfers { - transVals[i] = transferString(trans) - } - orderTransfers = fmt.Sprintf("[%s]", strings.Join(transVals, ", ")) - } - - feeInputs := "nil" - if s.FeeInputs != nil { - feeVals := make([]string, len(s.FeeInputs)) - for i, input := range s.FeeInputs { - feeVals[i] = bankInputString(input) - } - feeInputs = fmt.Sprintf("[%s]", strings.Join(feeVals, ", ")) - } - - return fmt.Sprintf("{OrderTransfers:%s, FeeInputs:%s}", orderTransfers, feeInputs) -} - -func TestBuildSettlementTransfers(t *testing.T) { - coin := func(amt int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amt)} +func TestPopulateFilled(t *testing.T) { + coin := func(amount int64, denom string) sdk.Coin { + return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amount)} } - igc := coin(2468, "ignorable") // igc => "ignorable coin" - askOrder := func(orderID uint64, seller string, assets, price sdk.Coin) *Order { + askOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { return NewOrder(orderID).WithAsk(&AskOrder{ - MarketId: 97531, - Seller: seller, - Assets: assets, - Price: price, + Assets: coin(assetsAmt, "acorn"), + Price: coin(priceAmt, "prune"), }) } - bidOrder := func(orderID uint64, buyer string, assets, price sdk.Coin) *Order { + bidOrder := func(orderID uint64, assetsAmt, priceAmt int64) *Order { return NewOrder(orderID).WithBid(&BidOrder{ - MarketId: 97531, - Buyer: buyer, - Assets: assets, - Price: price, + Assets: coin(assetsAmt, "acorn"), + Price: coin(priceAmt, "prune"), }) } - askSplit := func(orderID uint64, seller string, assets, price sdk.Coin) *OrderSplit { - return &OrderSplit{ - Order: &OrderFulfillment{Order: askOrder(orderID, seller, igc, igc)}, - Assets: assets, - Price: price, + newOF := func(order *Order, priceAppliedAmt int64, fees ...sdk.Coin) *orderFulfillment { + rv := &orderFulfillment{ + Order: order, + AssetsFilledAmt: order.GetAssets().Amount, + AssetsUnfilledAmt: sdkmath.ZeroInt(), + PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), + PriceLeftAmt: order.GetPrice().Amount.SubRaw(priceAppliedAmt), } - } - bidSplit := func(orderID uint64, seller string, assets, price sdk.Coin) *OrderSplit { - return &OrderSplit{ - Order: &OrderFulfillment{Order: bidOrder(orderID, seller, igc, igc)}, - Assets: assets, - Price: price, + if len(fees) > 0 { + rv.FeesToPay = fees } + return rv } - input := func(addr string, coins ...sdk.Coin) banktypes.Input { - return banktypes.Input{Address: addr, Coins: coins} - } - output := func(addr string, coins ...sdk.Coin) banktypes.Output { - return banktypes.Output{Address: addr, Coins: coins} - } - - tests := []struct { - name string - f *Fulfillments - expected *SettlementTransfers - expPanic string - }{ - { - name: "just an ask, no fees", - f: &Fulfillments{ - AskOFs: []*OrderFulfillment{ - { - Order: askOrder(1, "seller", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - bidSplit(6, "buyer", coin(7, "sasset"), coin(8, "sprice")), - bidSplit(9, "buyer", coin(10, "sasset"), coin(11, "sprice")), - }, - FeesToPay: nil, - }, - }, - }, - expected: &SettlementTransfers{ - OrderTransfers: []*Transfer{ - { - Inputs: []banktypes.Input{input("seller", coin(4, "oasset"))}, - Outputs: []banktypes.Output{output("buyer", coin(17, "sasset"))}, - }, - { - Inputs: []banktypes.Input{input("buyer", coin(19, "sprice"))}, - Outputs: []banktypes.Output{output("seller", coin(5, "oprice"))}, - }, - }, - FeeInputs: nil, - }, - }, - { - name: "just an ask, with fees", - f: &Fulfillments{ - AskOFs: []*OrderFulfillment{ - { - Order: askOrder(1, "seller", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - bidSplit(6, "buyer", coin(7, "sasset"), coin(8, "sprice")), - bidSplit(9, "buyer", coin(10, "sasset"), coin(11, "sprice")), - }, - FeesToPay: sdk.NewCoins(coin(12, "feea"), coin(13, "feeb")), - }, - }, - }, - expected: &SettlementTransfers{ - OrderTransfers: []*Transfer{ - { - Inputs: []banktypes.Input{input("seller", coin(4, "oasset"))}, - Outputs: []banktypes.Output{output("buyer", coin(17, "sasset"))}, - }, - { - Inputs: []banktypes.Input{input("buyer", coin(19, "sprice"))}, - Outputs: []banktypes.Output{output("seller", coin(5, "oprice"))}, - }, - }, - FeeInputs: []banktypes.Input{input("seller", coin(12, "feea"), coin(13, "feeb"))}, - }, - }, - { - name: "just a bid, no fees", - f: &Fulfillments{ - BidOFs: []*OrderFulfillment{ - { - Order: bidOrder(1, "buyer", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - askSplit(6, "seller", coin(7, "sasset"), coin(8, "sprice")), - askSplit(9, "seller", coin(10, "sasset"), coin(11, "sprice")), - }, - FeesToPay: nil, - }, - }, - }, - expected: &SettlementTransfers{ - OrderTransfers: []*Transfer{ - { - Inputs: []banktypes.Input{input("seller", coin(17, "sasset"))}, - Outputs: []banktypes.Output{output("buyer", coin(4, "oasset"))}, - }, - { - Inputs: []banktypes.Input{input("buyer", coin(5, "oprice"))}, - Outputs: []banktypes.Output{output("seller", coin(19, "sprice"))}, - }, - }, - FeeInputs: nil, - }, - }, + filledOrder := func(order *Order, actualPrice int64, actualFees ...sdk.Coin) *FilledOrder { + rv := &FilledOrder{ + order: order, + actualPrice: coin(actualPrice, order.GetPrice().Denom), + } + if len(actualFees) > 0 { + rv.actualFees = actualFees + } + return rv + } + + tests := []struct { + name string + askOFs []*orderFulfillment + bidOFs []*orderFulfillment + settlement *Settlement + expSettlement *Settlement + }{ { - name: "just a bid, with fees", - f: &Fulfillments{ - BidOFs: []*OrderFulfillment{ - { - Order: bidOrder(1, "buyer", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - askSplit(6, "seller", coin(7, "sasset"), coin(8, "sprice")), - askSplit(9, "seller", coin(10, "sasset"), coin(11, "sprice")), - }, - FeesToPay: sdk.NewCoins(coin(12, "feea"), coin(13, "feeb")), - }, - }, - }, - expected: &SettlementTransfers{ - OrderTransfers: []*Transfer{ - { - Inputs: []banktypes.Input{input("seller", coin(17, "sasset"))}, - Outputs: []banktypes.Output{output("buyer", coin(4, "oasset"))}, - }, - { - Inputs: []banktypes.Input{input("buyer", coin(5, "oprice"))}, - Outputs: []banktypes.Output{output("seller", coin(19, "sprice"))}, - }, - }, - FeeInputs: []banktypes.Input{input("buyer", coin(12, "feea"), coin(13, "feeb"))}, + name: "no partial", + askOFs: []*orderFulfillment{ + newOF(askOrder(2001, 53, 87), 92, coin(12, "fig")), + newOF(askOrder(2002, 17, 33), 37), + newOF(askOrder(2003, 22, 56), 60), }, - }, - { - name: "two asks two bids", - f: &Fulfillments{ - AskOFs: []*OrderFulfillment{ - { - Order: askOrder(1, "order seller", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - bidSplit(6, "split buyer one", coin(7, "sasset"), coin(8, "sprice")), - bidSplit(9, "split buyer two", coin(10, "sasset"), coin(11, "sprice")), - }, - FeesToPay: sdk.NewCoins(coin(12, "sellfee")), - }, - { - Order: askOrder(13, "order seller", coin(14, "oasset"), coin(15, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(16), - PriceAppliedAmt: sdkmath.NewInt(17), - Splits: []*OrderSplit{ - bidSplit(18, "split buyer one", coin(19, "sasset"), coin(20, "sprice")), - }, - FeesToPay: sdk.NewCoins(coin(21, "sellfee")), - }, - }, - BidOFs: []*OrderFulfillment{ - { - Order: bidOrder(22, "order buyer one", coin(23, "oasset"), coin(24, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(25), - PriceAppliedAmt: sdkmath.NewInt(26), - Splits: []*OrderSplit{ - askSplit(27, "split seller one", coin(28, "sasset"), coin(29, "sprice")), - askSplit(30, "split seller one", coin(31, "sasset"), coin(32, "sprice")), - }, - FeesToPay: sdk.NewCoins(coin(33, "buyfee")), - }, - { - Order: bidOrder(34, "order buyer two", coin(35, "oasset"), coin(36, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(37), - PriceAppliedAmt: sdkmath.NewInt(38), - Splits: []*OrderSplit{ - askSplit(39, "split seller one", coin(40, "sasset"), coin(41, "sprice")), - }, - FeesToPay: sdk.NewCoins(coin(42, "buyfee")), - }, - }, + bidOFs: []*orderFulfillment{ + newOF(bidOrder(3001, 30, 40), 40), + newOF(bidOrder(3002, 27, 49), 49, coin(39, "fig")), + newOF(bidOrder(3003, 35, 100), 100), }, - expected: &SettlementTransfers{ - OrderTransfers: []*Transfer{ - { - Inputs: []banktypes.Input{input("order seller", coin(4, "oasset"))}, - Outputs: []banktypes.Output{ - output("split buyer one", coin(7, "sasset")), - output("split buyer two", coin(10, "sasset")), - }, - }, - { - Inputs: []banktypes.Input{ - input("split buyer one", coin(8, "sprice")), - input("split buyer two", coin(11, "sprice")), - }, - Outputs: []banktypes.Output{output("order seller", coin(5, "oprice"))}, - }, - { - Inputs: []banktypes.Input{input("order seller", coin(16, "oasset"))}, - Outputs: []banktypes.Output{output("split buyer one", coin(19, "sasset"))}, - }, - { - Inputs: []banktypes.Input{input("split buyer one", coin(20, "sprice"))}, - Outputs: []banktypes.Output{output("order seller", coin(17, "oprice"))}, - }, - { - Inputs: []banktypes.Input{input("split seller one", coin(59, "sasset"))}, - Outputs: []banktypes.Output{output("order buyer one", coin(25, "oasset"))}, - }, - { - Inputs: []banktypes.Input{input("order buyer one", coin(26, "oprice"))}, - Outputs: []banktypes.Output{output("split seller one", coin(61, "sprice"))}, - }, - { - Inputs: []banktypes.Input{input("split seller one", coin(40, "sasset"))}, - Outputs: []banktypes.Output{output("order buyer two", coin(37, "oasset"))}, - }, - { - Inputs: []banktypes.Input{input("order buyer two", coin(38, "oprice"))}, - Outputs: []banktypes.Output{output("split seller one", coin(41, "sprice"))}, - }, - }, - FeeInputs: []banktypes.Input{ - input("order seller", coin(33, "sellfee")), - input("order buyer one", coin(33, "buyfee")), - input("order buyer two", coin(42, "buyfee")), + settlement: &Settlement{}, + expSettlement: &Settlement{ + FullyFilledOrders: []*FilledOrder{ + filledOrder(askOrder(2001, 53, 87), 92, coin(12, "fig")), + filledOrder(askOrder(2002, 17, 33), 37), + filledOrder(askOrder(2003, 22, 56), 60), + filledOrder(bidOrder(3001, 30, 40), 40), + filledOrder(bidOrder(3002, 27, 49), 49, coin(39, "fig")), + filledOrder(bidOrder(3003, 35, 100), 100), }, }, }, { - name: "negative ask asset", - f: &Fulfillments{ - AskOFs: []*OrderFulfillment{ - { - Order: askOrder(1, "seller", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(-4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - bidSplit(6, "buyer", coin(7, "sasset"), coin(8, "sprice")), - bidSplit(9, "buyer", coin(10, "sasset"), coin(11, "sprice")), - }, - }, - }, + name: "partial ask", + askOFs: []*orderFulfillment{ + newOF(askOrder(2001, 53, 87), 92, coin(12, "fig")), + newOF(askOrder(2002, 17, 33), 37), + newOF(askOrder(2003, 22, 56), 60), }, - expPanic: "invalid coin set -4oasset: coin -4oasset amount is not positive", - }, - { - name: "negative ask price", - f: &Fulfillments{ - AskOFs: []*OrderFulfillment{ - { - Order: askOrder(1, "seller", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(-5), - Splits: []*OrderSplit{ - bidSplit(6, "buyer", coin(7, "sasset"), coin(8, "sprice")), - bidSplit(9, "buyer", coin(10, "sasset"), coin(11, "sprice")), - }, - }, - }, + bidOFs: []*orderFulfillment{ + newOF(bidOrder(3001, 30, 40), 40), + newOF(bidOrder(3002, 27, 49), 49, coin(39, "fig")), + newOF(bidOrder(3003, 35, 100), 100), }, - expPanic: "invalid coin set -5oprice: coin -5oprice amount is not positive", - }, - { - name: "negative bid asset", - f: &Fulfillments{ - BidOFs: []*OrderFulfillment{ - { - Order: bidOrder(1, "buyer", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(-4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - askSplit(6, "seller", coin(7, "sasset"), coin(8, "sprice")), - askSplit(9, "seller", coin(10, "sasset"), coin(11, "sprice")), - }, - }, + settlement: &Settlement{PartialOrderLeft: askOrder(2002, 15, 63)}, + expSettlement: &Settlement{ + FullyFilledOrders: []*FilledOrder{ + filledOrder(askOrder(2001, 53, 87), 92, coin(12, "fig")), + filledOrder(askOrder(2003, 22, 56), 60), + filledOrder(bidOrder(3001, 30, 40), 40), + filledOrder(bidOrder(3002, 27, 49), 49, coin(39, "fig")), + filledOrder(bidOrder(3003, 35, 100), 100), }, + PartialOrderFilled: filledOrder(askOrder(2002, 17, 33), 37), + PartialOrderLeft: askOrder(2002, 15, 63), }, - expPanic: "invalid coin set -4oasset: coin -4oasset amount is not positive", }, { - name: "negative bid price", - f: &Fulfillments{ - BidOFs: []*OrderFulfillment{ - { - Order: bidOrder(1, "buyer", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(-5), - Splits: []*OrderSplit{ - askSplit(6, "seller", coin(7, "sasset"), coin(8, "sprice")), - askSplit(9, "seller", coin(10, "sasset"), coin(11, "sprice")), - }, - }, - }, + name: "partial bid", + askOFs: []*orderFulfillment{ + newOF(askOrder(2001, 53, 87), 92, coin(12, "fig")), + newOF(askOrder(2002, 17, 33), 37), + newOF(askOrder(2003, 22, 56), 60), }, - expPanic: "invalid coin set -5oprice: coin -5oprice amount is not positive", - }, - { - name: "ask with negative fees", - f: &Fulfillments{ - BidOFs: []*OrderFulfillment{ - { - Order: bidOrder(1, "buyer", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - askSplit(6, "seller", coin(7, "sasset"), coin(8, "sprice")), - askSplit(9, "seller", coin(10, "sasset"), coin(11, "sprice")), - }, - FeesToPay: sdk.Coins{coin(-12, "feecoin")}, - }, - }, + bidOFs: []*orderFulfillment{ + newOF(bidOrder(3001, 30, 40), 40), + newOF(bidOrder(3002, 27, 49), 49, coin(39, "fig")), + newOF(bidOrder(3003, 35, 100), 100), }, - expPanic: "invalid coin set -12feecoin: coin -12feecoin amount is not positive", - }, - { - name: "bid with negative fees", - f: &Fulfillments{ - BidOFs: []*OrderFulfillment{ - { - Order: bidOrder(1, "buyer", coin(2, "oasset"), coin(3, "oprice")), - AssetsFilledAmt: sdkmath.NewInt(4), - PriceAppliedAmt: sdkmath.NewInt(5), - Splits: []*OrderSplit{ - askSplit(6, "seller", coin(7, "sasset"), coin(8, "sprice")), - askSplit(9, "seller", coin(10, "sasset"), coin(11, "sprice")), - }, - FeesToPay: sdk.Coins{coin(-12, "feecoin")}, - }, + settlement: &Settlement{PartialOrderLeft: bidOrder(3003, 15, 63)}, + expSettlement: &Settlement{ + FullyFilledOrders: []*FilledOrder{ + filledOrder(askOrder(2001, 53, 87), 92, coin(12, "fig")), + filledOrder(askOrder(2002, 17, 33), 37), + filledOrder(askOrder(2003, 22, 56), 60), + filledOrder(bidOrder(3001, 30, 40), 40), + filledOrder(bidOrder(3002, 27, 49), 49, coin(39, "fig")), }, + PartialOrderFilled: filledOrder(bidOrder(3003, 35, 100), 100), + PartialOrderLeft: bidOrder(3003, 15, 63), }, - expPanic: "invalid coin set -12feecoin: coin -12feecoin amount is not positive", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var actual *SettlementTransfers - defer func() { - if t.Failed() { - t.Logf(" Actual: %s", settlementTransfersString(actual)) - t.Logf("Expected: %s", settlementTransfersString(tc.expected)) - t.Logf("Fulfillments: %s", fulfillmentsString(tc.f)) - } - }() testFunc := func() { - actual = BuildSettlementTransfers(tc.f) + populateFilled(tc.askOFs, tc.bidOFs, tc.settlement) } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "BuildSettlementTransfers") - assert.Equal(t, tc.expected, actual, "BuildSettlementTransfers result") + require.NotPanics(t, testFunc, "populateFilled") + assert.Equal(t, tc.expSettlement, tc.settlement, "settlement after populateFilled") }) } } -func TestGetAssetTransfer2(t *testing.T) { - coin := func(amt int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amt)} +func TestGetAssetTransfer(t *testing.T) { + seller, buyer := "sally", "brandon" + assetDenom, priceDenom := "apple", "peach" + newOF := func(order *Order, assetsFilledAmt int64, assetDists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ + Order: order, + AssetsFilledAmt: sdkmath.NewInt(assetsFilledAmt), + } + if len(assetDists) > 0 { + rv.AssetDists = assetDists + } + return rv } - igc := coin(2468, "ignorable") // igc => "ignorable coin" - askOrder := func(orderID uint64, seller string, assets sdk.Coin) *Order { + askOrder := func(orderID uint64) *Order { return NewOrder(orderID).WithAsk(&AskOrder{ - Seller: seller, - Assets: assets, + MarketId: 5555, + Seller: seller, + Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, + Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, }) } - bidOrder := func(orderID uint64, buyer string, assets sdk.Coin) *Order { + bidOrder := func(orderID uint64) *Order { return NewOrder(orderID).WithBid(&BidOrder{ - Buyer: buyer, - Assets: assets, + MarketId: 5555, + Buyer: buyer, + Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, + Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, }) } - orderSplit := func(order *Order, assets sdk.Coin) *OrderSplit { - return &OrderSplit{ - Order: &OrderFulfillment{Order: order}, - Assets: assets, - Price: igc, - } + dist := func(addr string, amount int64) *distribution { + return &distribution{Address: addr, Amount: sdkmath.NewInt(amount)} } - input := func(addr string, coins ...sdk.Coin) banktypes.Input { - return banktypes.Input{Address: addr, Coins: coins} + input := func(addr string, amount int64) banktypes.Input { + return banktypes.Input{ + Address: addr, + Coins: sdk.Coins{{Denom: assetDenom, Amount: sdkmath.NewInt(amount)}}, + } } - output := func(addr string, coins ...sdk.Coin) banktypes.Output { - return banktypes.Output{Address: addr, Coins: coins} + output := func(addr string, amount int64) banktypes.Output { + return banktypes.Output{ + Address: addr, + Coins: sdk.Coins{{Denom: assetDenom, Amount: sdkmath.NewInt(amount)}}, + } } tests := []struct { - name string - f *OrderFulfillment - exp *Transfer - expPanic string + name string + f *orderFulfillment + expTransfer *Transfer + expErr string + expPanic string }{ { - name: "ask, one split", - f: &OrderFulfillment{ - Order: askOrder(1, "seller", coin(25, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(33), - Splits: []*OrderSplit{orderSplit(bidOrder(2, "buyer", igc), coin(88, "banana"))}, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("seller", coin(33, "carrot"))}, - Outputs: []banktypes.Output{output("buyer", coin(88, "banana"))}, - }, + name: "nil inside order", + f: newOF(NewOrder(975), 5, dist("five", 5)), + expPanic: nilSubTypeErr(975), }, { - name: "ask, two splits diff addrs", - f: &OrderFulfillment{ - Order: askOrder(3, "SELLER", coin(26, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(4321), - Splits: []*OrderSplit{ - orderSplit(bidOrder(4, "buyer 1", igc), coin(89, "banana")), - orderSplit(bidOrder(5, "second buyer", igc), coin(45, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("SELLER", coin(4321, "carrot"))}, - Outputs: []banktypes.Output{ - output("buyer 1", coin(89, "banana")), - output("second buyer", coin(45, "apple")), - }, - }, + name: "unknown inside order", + f: newOF(newUnknownOrder(974), 5, dist("five", 5)), + expPanic: unknownSubTypeErr(974), }, { - name: "ask, two splits same addr, two denoms", - f: &OrderFulfillment{ - Order: askOrder(6, "SeLleR", coin(27, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(5511), - Splits: []*OrderSplit{ - orderSplit(bidOrder(7, "buyer", igc), coin(90, "banana")), - orderSplit(bidOrder(8, "buyer", igc), coin(46, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("SeLleR", coin(5511, "carrot"))}, - Outputs: []banktypes.Output{output("buyer", coin(46, "apple"), coin(90, "banana"))}, - }, + name: "assets filled negative: ask", + f: newOF(askOrder(159), -5), + expErr: "ask order 159 cannot be filled with \"-5apple\" assets: amount not positive", }, { - name: "ask, two splits same addr, one denom", - f: &OrderFulfillment{ - Order: askOrder(9, "sellsell", coin(28, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(42), - Splits: []*OrderSplit{ - orderSplit(bidOrder(10, "buybuy", igc), coin(55, "apple")), - orderSplit(bidOrder(11, "buybuy", igc), coin(34, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("sellsell", coin(42, "carrot"))}, - Outputs: []banktypes.Output{output("buybuy", coin(89, "apple"))}, - }, + name: "assets filled negative: bid", + f: newOF(bidOrder(953), -5), + expErr: "bid order 953 cannot be filled with \"-5apple\" assets: amount not positive", + }, + { + name: "assets filled zero: ask", + f: newOF(askOrder(991), 0), + expErr: "ask order 991 cannot be filled with \"0apple\" assets: amount not positive", + }, + { + name: "assets filled zero: bid", + f: newOF(bidOrder(992), 0), + expErr: "bid order 992 cannot be filled with \"0apple\" assets: amount not positive", + }, + { + name: "asset dists has negative amount: ask", + f: newOF(askOrder(549), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), + expErr: "ask order 549 cannot have \"-2apple\" assets in a transfer: amount not positive", + }, + { + name: "asset dists has negative amount: bid", + f: newOF(bidOrder(545), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), + expErr: "bid order 545 cannot have \"-2apple\" assets in a transfer: amount not positive", + }, + { + name: "asset dists has zero: ask", + f: newOF(askOrder(683), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), + expErr: "ask order 683 cannot have \"0apple\" assets in a transfer: amount not positive", + }, + { + name: "asset dists has zero: bid", + f: newOF(bidOrder(777), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), + expErr: "bid order 777 cannot have \"0apple\" assets in a transfer: amount not positive", + }, + { + name: "asset dists sum less than assets filled: ask", + f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), + expErr: "ask order 8 assets filled \"10apple\" does not equal assets distributed \"9apple\"", + }, + { + name: "asset dists sum less than assets filled: bid", + f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), + expErr: "bid order 3 assets filled \"10apple\" does not equal assets distributed \"9apple\"", + }, + { + name: "asset dists sum more than assets filled: ask", + f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), + expErr: "ask order 8 assets filled \"10apple\" does not equal assets distributed \"11apple\"", + }, + { + name: "asset dists sum more than assets filled: bid", + f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), + expErr: "bid order 3 assets filled \"10apple\" does not equal assets distributed \"11apple\"", }, { - name: "ask, negative price in split", - f: &OrderFulfillment{ - Order: askOrder(12, "goodsell", coin(29, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(91), - Splits: []*OrderSplit{orderSplit(bidOrder(13, "buygood", igc), coin(-4, "banana"))}, + name: "one dist: ask", + f: newOF(askOrder(12), 10, dist("ten", 10)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(seller, 10)}, + Outputs: []banktypes.Output{output("ten", 10)}, }, - expPanic: "cannot index and add invalid coin amount \"-4banana\"", }, { - name: "ask, negative price applied", - f: &OrderFulfillment{ - Order: askOrder(14, "solong", coin(30, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(-5), - Splits: []*OrderSplit{orderSplit(bidOrder(15, "hello", igc), coin(66, "banana"))}, + name: "one dist: bid", + f: newOF(bidOrder(13), 10, dist("ten", 10)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("ten", 10)}, + Outputs: []banktypes.Output{output(buyer, 10)}, }, - expPanic: "invalid coin set -5carrot: coin -5carrot amount is not positive", }, - { - name: "bid, one split", - f: &OrderFulfillment{ - Order: bidOrder(1, "buyer", coin(25, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(33), - Splits: []*OrderSplit{orderSplit(askOrder(2, "seller", igc), coin(88, "banana"))}, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("seller", coin(88, "banana"))}, - Outputs: []banktypes.Output{output("buyer", coin(33, "carrot"))}, + name: "two dists, different addresses: ask", + f: newOF(askOrder(2111), 20, dist("eleven", 11), dist("nine", 9)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(seller, 20)}, + Outputs: []banktypes.Output{output("eleven", 11), output("nine", 9)}, }, }, { - name: "bid, two splits diff addrs", - f: &OrderFulfillment{ - Order: bidOrder(3, "BUYER", coin(26, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(4321), - Splits: []*OrderSplit{ - orderSplit(askOrder(4, "seller 1", igc), coin(89, "banana")), - orderSplit(askOrder(5, "second seller", igc), coin(45, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{ - input("seller 1", coin(89, "banana")), - input("second seller", coin(45, "apple")), - }, - Outputs: []banktypes.Output{output("BUYER", coin(4321, "carrot"))}, + name: "two dists, different addresses: bid", + f: newOF(bidOrder(1222), 20, dist("eleven", 11), dist("nine", 9)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("eleven", 11), input("nine", 9)}, + Outputs: []banktypes.Output{output(buyer, 20)}, }, }, { - name: "bid, two splits same addr, two denoms", - f: &OrderFulfillment{ - Order: bidOrder(6, "BuYeR", coin(27, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(5511), - Splits: []*OrderSplit{ - orderSplit(askOrder(7, "seller", igc), coin(90, "banana")), - orderSplit(askOrder(8, "seller", igc), coin(46, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("seller", coin(46, "apple"), coin(90, "banana"))}, - Outputs: []banktypes.Output{output("BuYeR", coin(5511, "carrot"))}, + name: "two dists, same addresses: ask", + f: newOF(askOrder(5353), 52, dist("billy", 48), dist("billy", 4)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(seller, 52)}, + Outputs: []banktypes.Output{output("billy", 52)}, }, }, { - name: "bid, two splits same addr, one denom", - f: &OrderFulfillment{ - Order: bidOrder(9, "buybuy", coin(28, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(42), - Splits: []*OrderSplit{ - orderSplit(bidOrder(10, "sellsell", igc), coin(55, "apple")), - orderSplit(bidOrder(11, "sellsell", igc), coin(34, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("sellsell", coin(89, "apple"))}, - Outputs: []banktypes.Output{output("buybuy", coin(42, "carrot"))}, + name: "two dists, same addresses: bid", + f: newOF(bidOrder(3535), 52, dist("sol", 48), dist("sol", 4)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("sol", 52)}, + Outputs: []banktypes.Output{output(buyer, 52)}, }, }, { - name: "bid, negative price in split", - f: &OrderFulfillment{ - Order: bidOrder(12, "goodbuy", coin(29, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(91), - Splits: []*OrderSplit{orderSplit(askOrder(13, "sellgood", igc), coin(-4, "banana"))}, + name: "four dists: ask", + f: newOF(askOrder(99221), 33, + dist("buddy", 10), dist("brian", 13), dist("buddy", 8), dist("bella", 2)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(seller, 33)}, + Outputs: []banktypes.Output{output("buddy", 18), output("brian", 13), output("bella", 2)}, }, - expPanic: "cannot index and add invalid coin amount \"-4banana\"", }, { - name: "bid, negative price applied", - f: &OrderFulfillment{ - Order: bidOrder(14, "heythere", coin(30, "carrot")), - AssetsFilledAmt: sdkmath.NewInt(-5), - Splits: []*OrderSplit{orderSplit(askOrder(15, "afterwhile", igc), coin(66, "banana"))}, + name: "four dists: bid", + f: newOF(bidOrder(99221), 33, + dist("sydney", 10), dist("sarah", 2), dist("sydney", 8), dist("spencer", 13)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("sydney", 18), input("sarah", 2), input("spencer", 13)}, + Outputs: []banktypes.Output{output(buyer, 33)}, }, - expPanic: "invalid coin set -5carrot: coin -5carrot amount is not positive", - }, - - { - name: "nil inside order", - f: &OrderFulfillment{Order: NewOrder(20)}, - expPanic: "unknown order type ", - }, - { - name: "unknown inside order", - f: &OrderFulfillment{Order: newUnknownOrder(21)}, - expPanic: "unknown order type *exchange.unknownOrderType", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var actual *Transfer - defer func() { - if t.Failed() { - t.Logf(" Actual: %s", transferString(actual)) - t.Logf("Expected: %s", transferString(tc.exp)) - t.Logf("OrderFulfillment: %s", orderFulfillmentString(tc.f)) - } - }() + orig := copyOrderFulfillment(tc.f) + var transfer *Transfer + var err error testFunc := func() { - actual = GetAssetTransfer2(tc.f) + transfer, err = getAssetTransfer(tc.f) + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getAssetTransfer") + assertions.AssertErrorValue(t, err, tc.expErr, "getAssetTransfer error") + if !assert.Equal(t, tc.expTransfer, transfer, "getAssetTransfer transfers") { + t.Logf(" Actual: %s", transferString(transfer)) + t.Logf("Expected: %s", transferString(tc.expTransfer)) } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "GetAssetTransfer2") - assert.Equal(t, tc.exp, actual, "GetAssetTransfer2 result") + assertEqualOrderFulfillments(t, orig, tc.f, "orderFulfillment before and after getAssetTransfer") }) } } -func TestGetPriceTransfer2(t *testing.T) { - coin := func(amt int64, denom string) sdk.Coin { - return sdk.Coin{Denom: denom, Amount: sdkmath.NewInt(amt)} +func TestGetPriceTransfer(t *testing.T) { + seller, buyer := "sally", "brandon" + assetDenom, priceDenom := "apple", "peach" + newOF := func(order *Order, priceAppliedAmt int64, priceDists ...*distribution) *orderFulfillment { + rv := &orderFulfillment{ + Order: order, + PriceAppliedAmt: sdkmath.NewInt(priceAppliedAmt), + } + if len(priceDists) > 0 { + rv.PriceDists = priceDists + } + return rv } - igc := coin(2468, "ignorable") // igc => "ignorable coin" - askOrder := func(orderID uint64, seller string, price sdk.Coin) *Order { + askOrder := func(orderID uint64) *Order { return NewOrder(orderID).WithAsk(&AskOrder{ - Seller: seller, - Price: price, + MarketId: 5555, + Seller: seller, + Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, + Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, }) } - bidOrder := func(orderID uint64, buyer string, price sdk.Coin) *Order { + bidOrder := func(orderID uint64) *Order { return NewOrder(orderID).WithBid(&BidOrder{ - Buyer: buyer, - Price: price, + MarketId: 5555, + Buyer: buyer, + Assets: sdk.Coin{Denom: assetDenom, Amount: sdkmath.NewInt(111)}, + Price: sdk.Coin{Denom: priceDenom, Amount: sdkmath.NewInt(999)}, }) } - orderSplit := func(order *Order, price sdk.Coin) *OrderSplit { - return &OrderSplit{ - Order: &OrderFulfillment{Order: order}, - Price: price, - Assets: igc, - } + dist := func(addr string, amount int64) *distribution { + return &distribution{Address: addr, Amount: sdkmath.NewInt(amount)} } - input := func(addr string, coins ...sdk.Coin) banktypes.Input { - return banktypes.Input{Address: addr, Coins: coins} + input := func(addr string, amount int64) banktypes.Input { + return banktypes.Input{ + Address: addr, + Coins: sdk.Coins{{Denom: priceDenom, Amount: sdkmath.NewInt(amount)}}, + } } - output := func(addr string, coins ...sdk.Coin) banktypes.Output { - return banktypes.Output{Address: addr, Coins: coins} + output := func(addr string, amount int64) banktypes.Output { + return banktypes.Output{ + Address: addr, + Coins: sdk.Coins{{Denom: priceDenom, Amount: sdkmath.NewInt(amount)}}, + } } tests := []struct { - name string - f *OrderFulfillment - exp *Transfer - expPanic string + name string + f *orderFulfillment + expTransfer *Transfer + expErr string + expPanic string }{ { - name: "ask, one split", - f: &OrderFulfillment{ - Order: askOrder(1, "seller", coin(25, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(33), - Splits: []*OrderSplit{orderSplit(bidOrder(2, "buyer", igc), coin(88, "banana"))}, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("buyer", coin(88, "banana"))}, - Outputs: []banktypes.Output{output("seller", coin(33, "carrot"))}, - }, + name: "nil inside order", + f: newOF(NewOrder(975), 5, dist("five", 5)), + expPanic: nilSubTypeErr(975), }, { - name: "ask, two splits diff addrs", - f: &OrderFulfillment{ - Order: askOrder(3, "SELLER", coin(26, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(4321), - Splits: []*OrderSplit{ - orderSplit(bidOrder(4, "buyer 1", igc), coin(89, "banana")), - orderSplit(bidOrder(5, "second buyer", igc), coin(45, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{ - input("buyer 1", coin(89, "banana")), - input("second buyer", coin(45, "apple")), - }, - Outputs: []banktypes.Output{output("SELLER", coin(4321, "carrot"))}, - }, + name: "unknown inside order", + f: newOF(newUnknownOrder(974), 5, dist("five", 5)), + expPanic: unknownSubTypeErr(974), }, { - name: "ask, two splits same addr, two denoms", - f: &OrderFulfillment{ - Order: askOrder(6, "SeLleR", coin(27, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(5511), - Splits: []*OrderSplit{ - orderSplit(bidOrder(7, "buyer", igc), coin(90, "banana")), - orderSplit(bidOrder(8, "buyer", igc), coin(46, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("buyer", coin(46, "apple"), coin(90, "banana"))}, - Outputs: []banktypes.Output{output("SeLleR", coin(5511, "carrot"))}, - }, + name: "price applied negative: ask", + f: newOF(askOrder(159), -5), + expErr: "ask order 159 cannot be filled at price \"-5peach\": amount not positive", }, { - name: "ask, two splits same addr, one denom", - f: &OrderFulfillment{ - Order: askOrder(9, "sellsell", coin(28, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(42), - Splits: []*OrderSplit{ - orderSplit(bidOrder(10, "buybuy", igc), coin(55, "apple")), - orderSplit(bidOrder(11, "buybuy", igc), coin(34, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("buybuy", coin(89, "apple"))}, - Outputs: []banktypes.Output{output("sellsell", coin(42, "carrot"))}, - }, + name: "price applied negative: bid", + f: newOF(bidOrder(953), -5), + expErr: "bid order 953 cannot be filled at price \"-5peach\": amount not positive", }, { - name: "ask, negative price in split", - f: &OrderFulfillment{ - Order: askOrder(12, "goodsell", coin(29, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(91), - Splits: []*OrderSplit{orderSplit(bidOrder(13, "buygood", igc), coin(-4, "banana"))}, - }, - expPanic: "cannot index and add invalid coin amount \"-4banana\"", + name: "price applied zero: ask", + f: newOF(askOrder(991), 0), + expErr: "ask order 991 cannot be filled at price \"0peach\": amount not positive", }, { - name: "ask, negative price applied", - f: &OrderFulfillment{ - Order: askOrder(14, "solong", coin(30, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(-5), - Splits: []*OrderSplit{orderSplit(bidOrder(15, "hello", igc), coin(66, "banana"))}, - }, - expPanic: "invalid coin set -5carrot: coin -5carrot amount is not positive", + name: "price applied zero: bid", + f: newOF(bidOrder(992), 0), + expErr: "bid order 992 cannot be filled at price \"0peach\": amount not positive", }, - { - name: "bid, one split", - f: &OrderFulfillment{ - Order: bidOrder(1, "buyer", coin(25, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(33), - Splits: []*OrderSplit{orderSplit(askOrder(2, "seller", igc), coin(88, "banana"))}, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("buyer", coin(33, "carrot"))}, - Outputs: []banktypes.Output{output("seller", coin(88, "banana"))}, - }, + name: "price dists has negative amount: ask", + f: newOF(askOrder(549), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), + expErr: "ask order 549 cannot have price \"-2peach\" in a transfer: amount not positive", }, { - name: "bid, two splits diff addrs", - f: &OrderFulfillment{ - Order: bidOrder(3, "BUYER", coin(26, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(4321), - Splits: []*OrderSplit{ - orderSplit(askOrder(4, "seller 1", igc), coin(89, "banana")), - orderSplit(askOrder(5, "second seller", igc), coin(45, "apple")), - }, - }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("BUYER", coin(4321, "carrot"))}, - Outputs: []banktypes.Output{ - output("seller 1", coin(89, "banana")), - output("second seller", coin(45, "apple")), - }, - }, + name: "price dists has negative amount: bid", + f: newOF(bidOrder(545), 10, dist("one", 1), dist("two", 2), dist("neg", -2), dist("nine", 9)), + expErr: "bid order 545 cannot have price \"-2peach\" in a transfer: amount not positive", }, { - name: "bid, two splits same addr, two denoms", - f: &OrderFulfillment{ - Order: bidOrder(6, "BuYeR", coin(27, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(5511), - Splits: []*OrderSplit{ - orderSplit(askOrder(7, "seller", igc), coin(90, "banana")), - orderSplit(askOrder(8, "seller", igc), coin(46, "apple")), - }, + name: "price dists has zero: ask", + f: newOF(askOrder(683), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), + expErr: "ask order 683 cannot have price \"0peach\" in a transfer: amount not positive", + }, + { + name: "price dists has zero: bid", + f: newOF(bidOrder(777), 10, dist("one", 1), dist("two", 2), dist("zero", 0), dist("seven", 7)), + expErr: "bid order 777 cannot have price \"0peach\" in a transfer: amount not positive", + }, + { + name: "price dists sum less than price applied: ask", + f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), + expErr: "ask order 8 price filled \"10peach\" does not equal price distributed \"9peach\"", + }, + { + name: "price dists sum less than price applied: bid", + f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("three2", 3)), + expErr: "bid order 3 price filled \"10peach\" does not equal price distributed \"9peach\"", + }, + { + name: "price dists sum more than price applied: ask", + f: newOF(askOrder(8), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), + expErr: "ask order 8 price filled \"10peach\" does not equal price distributed \"11peach\"", + }, + { + name: "price dists sum more than price applied: bid", + f: newOF(bidOrder(3), 10, dist("one", 1), dist("two", 2), dist("three", 3), dist("five", 5)), + expErr: "bid order 3 price filled \"10peach\" does not equal price distributed \"11peach\"", + }, + { + name: "one dist: ask", + f: newOF(askOrder(12), 10, dist("ten", 10)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("ten", 10)}, + Outputs: []banktypes.Output{output(seller, 10)}, }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("BuYeR", coin(5511, "carrot"))}, - Outputs: []banktypes.Output{output("seller", coin(46, "apple"), coin(90, "banana"))}, + }, + { + name: "one dist: bid", + f: newOF(bidOrder(13), 10, dist("ten", 10)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(buyer, 10)}, + Outputs: []banktypes.Output{output("ten", 10)}, }, }, { - name: "bid, two splits same addr, one denom", - f: &OrderFulfillment{ - Order: bidOrder(9, "buybuy", coin(28, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(42), - Splits: []*OrderSplit{ - orderSplit(bidOrder(10, "sellsell", igc), coin(55, "apple")), - orderSplit(bidOrder(11, "sellsell", igc), coin(34, "apple")), - }, + name: "two dists, different addresses: ask", + f: newOF(askOrder(2111), 20, dist("eleven", 11), dist("nine", 9)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("eleven", 11), input("nine", 9)}, + Outputs: []banktypes.Output{output(seller, 20)}, }, - exp: &Transfer{ - Inputs: []banktypes.Input{input("buybuy", coin(42, "carrot"))}, - Outputs: []banktypes.Output{output("sellsell", coin(89, "apple"))}, + }, + { + name: "two dists, different addresses: bid", + f: newOF(bidOrder(1222), 20, dist("eleven", 11), dist("nine", 9)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(buyer, 20)}, + Outputs: []banktypes.Output{output("eleven", 11), output("nine", 9)}, }, }, { - name: "bid, negative price in split", - f: &OrderFulfillment{ - Order: bidOrder(12, "goodbuy", coin(29, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(91), - Splits: []*OrderSplit{orderSplit(askOrder(13, "sellgood", igc), coin(-4, "banana"))}, + name: "two dists, same addresses: ask", + f: newOF(askOrder(5353), 52, dist("billy", 48), dist("billy", 4)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("billy", 52)}, + Outputs: []banktypes.Output{output(seller, 52)}, }, - expPanic: "cannot index and add invalid coin amount \"-4banana\"", }, { - name: "bid, negative price applied", - f: &OrderFulfillment{ - Order: bidOrder(14, "heythere", coin(30, "carrot")), - PriceAppliedAmt: sdkmath.NewInt(-5), - Splits: []*OrderSplit{orderSplit(askOrder(15, "afterwhile", igc), coin(66, "banana"))}, + name: "two dists, same addresses: bid", + f: newOF(bidOrder(3535), 52, dist("sol", 48), dist("sol", 4)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(buyer, 52)}, + Outputs: []banktypes.Output{output("sol", 52)}, }, - expPanic: "invalid coin set -5carrot: coin -5carrot amount is not positive", }, - { - name: "nil inside order", - f: &OrderFulfillment{Order: NewOrder(20)}, - expPanic: "unknown order type ", + name: "four dists: ask", + f: newOF(askOrder(99221), 33, + dist("buddy", 10), dist("brian", 13), dist("buddy", 8), dist("bella", 2)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input("buddy", 18), input("brian", 13), input("bella", 2)}, + Outputs: []banktypes.Output{output(seller, 33)}, + }, }, { - name: "unknown inside order", - f: &OrderFulfillment{Order: newUnknownOrder(21)}, - expPanic: "unknown order type *exchange.unknownOrderType", + name: "four dists: bid", + f: newOF(bidOrder(99221), 33, + dist("sydney", 10), dist("sarah", 2), dist("sydney", 8), dist("spencer", 13)), + expTransfer: &Transfer{ + Inputs: []banktypes.Input{input(buyer, 33)}, + Outputs: []banktypes.Output{output("sydney", 18), output("sarah", 2), output("spencer", 13)}, + }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - var actual *Transfer - defer func() { - if t.Failed() { - t.Logf(" Actual: %s", transferString(actual)) - t.Logf("Expected: %s", transferString(tc.exp)) - t.Logf("OrderFulfillment: %s", orderFulfillmentString(tc.f)) - } - }() + orig := copyOrderFulfillment(tc.f) + var transfer *Transfer + var err error testFunc := func() { - actual = GetPriceTransfer2(tc.f) + transfer, err = getPriceTransfer(tc.f) + } + assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "getPriceTransfer") + assertions.AssertErrorValue(t, err, tc.expErr, "getPriceTransfer error") + if !assert.Equal(t, tc.expTransfer, transfer, "getPriceTransfer transfers") { + t.Logf(" Actual: %s", transferString(transfer)) + t.Logf("Expected: %s", transferString(tc.expTransfer)) } - assertions.RequirePanicEquals(t, testFunc, tc.expPanic, "GetPriceTransfer2") - assert.Equal(t, tc.exp, actual, "GetPriceTransfer2 result") + assertEqualOrderFulfillments(t, orig, tc.f, "orderFulfillment before and after getPriceTransfer") }) } }