From db50acb24573b0e49cb1ca4b844dd61bb608a13d Mon Sep 17 00:00:00 2001 From: ben2077 Date: Fri, 5 Jul 2024 06:47:06 +0800 Subject: [PATCH] test liquidity provider --- server/internal/core/application/service.go | 96 +++++--- .../core/domain/liquidity_provider.go | 16 ++ server/internal/core/domain/round.go | 32 +-- .../tx-builder/covenant/builder.go | 227 ++++++++++++++++++ 4 files changed, 327 insertions(+), 44 deletions(-) create mode 100644 server/internal/core/domain/liquidity_provider.go diff --git a/server/internal/core/application/service.go b/server/internal/core/application/service.go index da876b8a3..971cf78bf 100644 --- a/server/internal/core/application/service.go +++ b/server/internal/core/application/service.go @@ -5,6 +5,7 @@ import ( "context" "encoding/hex" "fmt" + txbuilder "github.com/ark-network/ark/internal/infrastructure/tx-builder/covenant" "sync" "time" @@ -385,41 +386,78 @@ func (s *service) startFinalization() { return } - sweptRounds, err := s.repoManager.Rounds().GetSweptRounds(ctx) - if err != nil { - changes = round.Fail(fmt.Errorf("failed to retrieve swept rounds: %s", err)) - log.WithError(err).Warn("failed to retrieve swept rounds") - return - } + if round.RequiresLiquidityProvider { + fmt.Println("This round requires a liquidity provider.") - unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds) - if err != nil { - changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) - log.WithError(err).Warn("failed to create pool tx") - return - } - log.Debugf("pool tx created for round %s", round.Id) + liquidityTxBuilder := txbuilder.NewLiquidityTxBuilder(s.builder) - // TODO BTC make the senders sign the tree + unsignedPoolTx, tree, connectorAddress, err := liquidityTxBuilder.BuildPoolTxFromLiquidityProvider(payments, s.minRelayFee, round.LiquidityProvider, s.pubkey) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) + log.WithError(err).Warn("failed to create pool tx") + return + } + log.Debugf("pool tx created for round %s", round.Id) - connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee) - if err != nil { - changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) - log.WithError(err).Warn("failed to create connectors and forfeit txs") - return - } + // TODO BTC make the senders sign the tree - log.Debugf("forfeit transactions created for round %s", round.Id) + connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(round.LiquidityProvider.PubKey, unsignedPoolTx, payments, s.minRelayFee) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) + log.WithError(err).Warn("failed to create connectors and forfeit txs") + return + } - events, err := round.StartFinalization(connectorAddress, connectors, tree, unsignedPoolTx) - if err != nil { - changes = round.Fail(fmt.Errorf("failed to start finalization: %s", err)) - log.WithError(err).Warn("failed to start finalization") - return - } - changes = append(changes, events...) + log.Debugf("forfeit transactions created for round %s", round.Id) + + events, err := round.StartFinalization(connectorAddress, connectors, tree, unsignedPoolTx) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to start finalization: %s", err)) + log.WithError(err).Warn("failed to start finalization") + return + } + changes = append(changes, events...) + + s.forfeitTxs.push(forfeitTxs) + } else { + fmt.Println("This round does not require a liquidity provider.") + + sweptRounds, err := s.repoManager.Rounds().GetSweptRounds(ctx) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to retrieve swept rounds: %s", err)) + log.WithError(err).Warn("failed to retrieve swept rounds") + return + } + + unsignedPoolTx, tree, connectorAddress, err := s.builder.BuildPoolTx(s.pubkey, payments, s.minRelayFee, sweptRounds) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to create pool tx: %s", err)) + log.WithError(err).Warn("failed to create pool tx") + return + } + log.Debugf("pool tx created for round %s", round.Id) + + // TODO BTC make the senders sign the tree - s.forfeitTxs.push(forfeitTxs) + connectors, forfeitTxs, err := s.builder.BuildForfeitTxs(s.pubkey, unsignedPoolTx, payments, s.minRelayFee) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to create connectors and forfeit txs: %s", err)) + log.WithError(err).Warn("failed to create connectors and forfeit txs") + return + } + + log.Debugf("forfeit transactions created for round %s", round.Id) + + events, err := round.StartFinalization(connectorAddress, connectors, tree, unsignedPoolTx) + if err != nil { + changes = round.Fail(fmt.Errorf("failed to start finalization: %s", err)) + log.WithError(err).Warn("failed to start finalization") + return + } + changes = append(changes, events...) + + s.forfeitTxs.push(forfeitTxs) + } log.Debugf("started finalization stage for round: %s", round.Id) } diff --git a/server/internal/core/domain/liquidity_provider.go b/server/internal/core/domain/liquidity_provider.go new file mode 100644 index 000000000..8c344e36c --- /dev/null +++ b/server/internal/core/domain/liquidity_provider.go @@ -0,0 +1,16 @@ +package domain + +import ( + "github.com/ark-network/ark/internal/core/ports" + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +type LiquidityProvider struct { + PubKey *secp256k1.PublicKey + UTXO []ports.TxInput + FeeRate uint64 +} + +func (lp *LiquidityProvider) CheckFeeRate(minRate, maxRate uint64) bool { + return lp.FeeRate >= minRate && lp.FeeRate <= maxRate +} diff --git a/server/internal/core/domain/round.go b/server/internal/core/domain/round.go index 9a96f76b3..01f737e63 100644 --- a/server/internal/core/domain/round.go +++ b/server/internal/core/domain/round.go @@ -34,21 +34,23 @@ type Stage struct { } type Round struct { - Id string - StartingTimestamp int64 - EndingTimestamp int64 - Stage Stage - Payments map[string]Payment - Txid string - UnsignedTx string - ForfeitTxs []string - CongestionTree tree.CongestionTree - Connectors []string - ConnectorAddress string - DustAmount uint64 - Version uint - Swept bool // true if all the vtxos are vtxo.Swept or vtxo.Redeemed - changes []RoundEvent + Id string + StartingTimestamp int64 + EndingTimestamp int64 + Stage Stage + Payments map[string]Payment + Txid string + UnsignedTx string + ForfeitTxs []string + CongestionTree tree.CongestionTree + Connectors []string + ConnectorAddress string + DustAmount uint64 + Version uint + Swept bool // true if all the vtxos are vtxo.Swept or vtxo.Redeemed + RequiresLiquidityProvider bool // true if this round requires a liquidity provider + LiquidityProvider *LiquidityProvider + changes []RoundEvent } func NewRound(dustAmount uint64) *Round { diff --git a/server/internal/infrastructure/tx-builder/covenant/builder.go b/server/internal/infrastructure/tx-builder/covenant/builder.go index e7aa58e26..d90ec02de 100644 --- a/server/internal/infrastructure/tx-builder/covenant/builder.go +++ b/server/internal/infrastructure/tx-builder/covenant/builder.go @@ -29,6 +29,16 @@ type txBuilder struct { exitDelay int64 // in seconds } +type LiquidityTxBuilder struct { + o *txBuilder +} + +func NewLiquidityTxBuilder(builder ports.TxBuilder) *LiquidityTxBuilder { + return &LiquidityTxBuilder{ + o: builder.(*txBuilder), + } +} + func NewTxBuilder( wallet ports.WalletService, net network.Network, @@ -172,6 +182,57 @@ func (b *txBuilder) BuildPoolTx( return } +func (l *LiquidityTxBuilder) BuildPoolTxFromLiquidityProvider(payments []domain.Payment, minRelayFee uint64, liquidityProvider *domain.LiquidityProvider, aspPubKey *secp256k1.PublicKey, +) (poolTx string, congestionTree tree.CongestionTree, connectorAddress string, err error) { + + var sharedOutputScript []byte + var sharedOutputAmount uint64 + var treeFactoryFn tree.TreeFactory + + if !isOnchainOnly(payments) { + treeFactoryFn, sharedOutputScript, sharedOutputAmount, err = tree.CraftCongestionTree( + l.o.net.AssetID, liquidityProvider.PubKey, getOffchainReceivers(payments), minRelayFee, l.o.roundLifetime, l.o.exitDelay, + ) + if err != nil { + return + } + } + + connectorAddress, err = l.o.wallet.DeriveConnectorAddress(context.Background()) + if err != nil { + return + } + + ptx, err := l.createLiquidityPoolTx( + sharedOutputAmount, sharedOutputScript, payments, connectorAddress, minRelayFee, *liquidityProvider, aspPubKey, + ) + if err != nil { + return + } + + unsignedTx, err := ptx.UnsignedTx() + if err != nil { + return + } + + if treeFactoryFn != nil { + congestionTree, err = treeFactoryFn(psetv2.InputArgs{ + Txid: unsignedTx.TxHash().String(), + TxIndex: 0, + }) + if err != nil { + return + } + } + + poolTx, err = ptx.ToBase64() + if err != nil { + return + } + + return +} + func (b *txBuilder) GetSweepInput(parentblocktime int64, node tree.Node) (expirationtime int64, sweepInput ports.SweepInput, err error) { pset, err := psetv2.NewPsetFromBase64(node.Tx) if err != nil { @@ -250,6 +311,172 @@ func (b *txBuilder) getLeafScriptAndTree( return outputScript, taprootTree, nil } +func (l *LiquidityTxBuilder) createLiquidityPoolTx( + sharedOutputAmount uint64, + sharedOutputScript []byte, + payments []domain.Payment, + connectorAddress string, + minRelayFee uint64, + provider domain.LiquidityProvider, + aspPubKey *secp256k1.PublicKey, +) (*psetv2.Pset, error) { + + connectorScript, err := address.ToOutputScript(connectorAddress) + if err != nil { + return nil, err + } + + receivers := getOnchainReceivers(payments) + nbOfInputs := countSpentVtxos(payments) + connectorsAmount := (connectorAmount + minRelayFee) * nbOfInputs + if nbOfInputs > 1 { + connectorsAmount -= minRelayFee + } + targetAmount := connectorsAmount + + outputs := make([]psetv2.OutputArgs, 0) + + if sharedOutputScript != nil && sharedOutputAmount > 0 { + targetAmount += sharedOutputAmount + + outputs = append(outputs, psetv2.OutputArgs{ + Asset: l.o.net.AssetID, + Amount: sharedOutputAmount, + Script: sharedOutputScript, + }) + } + + outputs = append(outputs, psetv2.OutputArgs{ + Asset: l.o.net.AssetID, + Amount: connectorsAmount, + Script: connectorScript, + }) + + for _, receiver := range receivers { + targetAmount += receiver.Amount + + receiverScript, err := address.ToOutputScript(receiver.OnchainAddress) + if err != nil { + return nil, err + } + + outputs = append(outputs, psetv2.OutputArgs{ + Asset: l.o.net.AssetID, + Amount: receiver.Amount, + Script: receiverScript, + }) + } + + ctx := context.Background() + + // Initialize the partial transaction + ptx, err := psetv2.New(nil, outputs, nil) + if err != nil { + return nil, err + } + + updater, err := psetv2.NewUpdater(ptx) + if err != nil { + return nil, err + } + + // Add inputs from the liquidity provider + if err := addInputs(updater, provider.UTXO); err != nil { + return nil, err + } + + // Estimate the fees + b64, err := ptx.ToBase64() + if err != nil { + return nil, err + } + + feeAmount, err := l.o.wallet.EstimateFees(ctx, b64) + if err != nil { + return nil, err + } + + // Calculate the provider's fee + providerFee := targetAmount * provider.FeeRate / 1000000 + feeAmount += providerFee + + // Calculate the ASP fee + aspFee := feeAmount - providerFee + + // Calculate the total amount of provided UTXOs + totalProvided := uint64(0) + for _, utxo := range provider.UTXO { + totalProvided += utxo.GetValue() + } + + // Calculate the change amount + changeAmount := totalProvided - targetAmount - feeAmount + + // If there is change, add a change output + if changeAmount > 0 { + changeScript, err := p2wpkhScript(provider.PubKey, l.o.net) + if err != nil { + return nil, err + } + + if changeAmount < dustLimit { + // If change is less than dust limit, add it to fee + aspFee += changeAmount + } else { + // Add change output + outputs = append(outputs, psetv2.OutputArgs{ + Asset: l.o.net.AssetID, + Amount: changeAmount, + Script: changeScript, + }) + + if err := updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: l.o.net.AssetID, + Amount: changeAmount, + Script: changeScript, + }, + }); err != nil { + return nil, err + } + } + } + + // Add fee output for the ASP + aspScript, err := p2wpkhScript(aspPubKey, l.o.net) + if err != nil { + return nil, err + } + + if err := updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: l.o.net.AssetID, + Amount: aspFee, + Script: aspScript, // ASP's script + }, + }); err != nil { + return nil, err + } + + // Add fee output for the Liquidity Provider + providerScript, err := p2wpkhScript(provider.PubKey, l.o.net) + if err != nil { + return nil, err + } + + if err := updater.AddOutputs([]psetv2.OutputArgs{ + { + Asset: l.o.net.AssetID, + Amount: providerFee, + Script: providerScript, + }, + }); err != nil { + return nil, err + } + + return ptx, nil +} + func (b *txBuilder) createPoolTx( sharedOutputAmount uint64, sharedOutputScript []byte, payments []domain.Payment, aspPubKey *secp256k1.PublicKey, connectorAddress string, minRelayFee uint64,