Skip to content

Commit

Permalink
feat: allow external pay for reverse swaps (#121)
Browse files Browse the repository at this point in the history
* feat: allow external pay for reverse swaps

* fix: validate request parameters before swap creation
  • Loading branch information
jackstar12 committed Mar 20, 2024
1 parent 97f0ad8 commit d23447c
Show file tree
Hide file tree
Showing 10 changed files with 651 additions and 530 deletions.
1,054 changes: 545 additions & 509 deletions boltzrpc/boltzrpc.pb.go

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion boltzrpc/boltzrpc.proto
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ message ReverseSwapInfo {
optional uint64 service_fee = 18;
optional uint64 onchain_fee = 19;
optional uint64 routing_fee_msat = 20;
bool external_pay = 21;
}

message BlockHeights {
Expand Down Expand Up @@ -417,7 +418,9 @@ message CreateReverseSwapRequest {
optional string wallet = 6;
// Whether the daemon should return immediately after creating the swap or wait until the swap is successful or failed.
// It will always return immediately if `accept_zero_conf` is not set.
bool return_immediately = 7;
optional bool return_immediately = 7;
// If set, the daemon will not pay the invoice of the swap and return the invoice to be paid. This implicitly sets `return_immediately` to true.
optional bool external_pay = 8;
}
message CreateReverseSwapResponse {
string id = 1;
Expand All @@ -427,6 +430,8 @@ message CreateReverseSwapResponse {
optional uint64 routing_fee_milli_sat = 3;
// Only populated when zero-conf is accepted and return_immediately is set to false
optional string claim_transaction_id = 4;
// Invoice to be paid. Only populated when `external_pay` is set to true
optional string invoice = 5;
}

message ChannelId {
Expand Down
12 changes: 11 additions & 1 deletion cmd/boltzcli/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,9 @@ func swapInfoStream(ctx *cli.Context, id string, json bool) error {
switch status {
case boltz.SwapCreated:
fmt.Printf("Swap ID: %s\n", swap.Id)
if swap.ExternalPay {
fmt.Printf("Invoice: %s\n", swap.Invoice)
}
case boltz.TransactionMempool:
fmt.Printf("Lockup Transaction ID: %s\n", swap.LockupTransactionId)
case boltz.InvoiceSettled:
Expand Down Expand Up @@ -910,6 +913,10 @@ var createReverseSwapCommand = &cli.Command{
Name: "no-zero-conf",
Usage: "Disable zero-conf for this swap",
},
&cli.BoolFlag{
Name: "external-pay",
Usage: "Do not automatically pay the swap from the connected lightning node",
},
&cli.StringSliceFlag{
Name: "chan-id",
},
Expand Down Expand Up @@ -970,14 +977,17 @@ func createReverseSwap(ctx *cli.Context) error {
}

wallet := ctx.String("wallet")
externalPay := ctx.Bool("external-pay")
returnImmediately := true
response, err := client.CreateReverseSwap(&boltzrpc.CreateReverseSwapRequest{
Address: address,
Amount: amount,
AcceptZeroConf: !ctx.Bool("no-zero-conf"),
Pair: pair,
Wallet: &wallet,
ChanIds: ctx.StringSlice("chan-id"),
ReturnImmediately: true,
ReturnImmediately: &returnImmediately,
ExternalPay: &externalPay,
})
if err != nil {
return err
Expand Down
51 changes: 45 additions & 6 deletions cmd/boltzd/boltzd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,11 @@ func parseCurrency(grpcCurrency boltzrpc.Currency) boltz.Currency {
}
}

var pairBtc = &boltzrpc.Pair{
From: boltzrpc.Currency_BTC,
To: boltzrpc.Currency_BTC,
}

func TestSwap(t *testing.T) {
nodes := []string{"CLN", "LND"}

Expand All @@ -303,11 +308,6 @@ func TestSwap(t *testing.T) {
Liquid: &onchain.Currency{Tx: onchain.NewBoltzTxProvider(boltzClient, boltz.CurrencyLiquid)},
}

pairBtc := &boltzrpc.Pair{
From: boltzrpc.Currency_BTC,
To: boltzrpc.Currency_BTC,
}

checkSwap := func(t *testing.T, swap *boltzrpc.SwapInfo) {
invoice, err := zpay32.Decode(swap.Invoice, &chaincfg.RegressionNetParams)
require.NoError(t, err)
Expand Down Expand Up @@ -640,12 +640,13 @@ func TestReverseSwap(t *testing.T) {

var info *boltzrpc.GetSwapInfoResponse

returnImmediately := !tc.waitForClaim
request := &boltzrpc.CreateReverseSwapRequest{
Amount: 100000,
Address: addr,
Pair: pair,
AcceptZeroConf: tc.zeroConf,
ReturnImmediately: !tc.waitForClaim,
ReturnImmediately: &returnImmediately,
}

if tc.recover {
Expand Down Expand Up @@ -714,6 +715,44 @@ func TestReverseSwap(t *testing.T) {
}
})
}

t.Run("ExternalPay", func(t *testing.T) {
cfg := loadConfig(t)
client, _, stop := setup(t, cfg, "")
defer stop()

externalPay := true
returnImmediately := false

request := &boltzrpc.CreateReverseSwapRequest{
Amount: 100000,
AcceptZeroConf: true,
ExternalPay: &externalPay,
ReturnImmediately: &returnImmediately,
}

// cant wait for claim transaction if paid externally
_, err := client.CreateReverseSwap(request)
require.Error(t, err)

request.ReturnImmediately = nil

swap, err := client.CreateReverseSwap(request)
require.NoError(t, err)
require.NotEmpty(t, swap.Invoice)

stream := swapStream(t, client, swap.Id)

_, err = cfg.Lightning.PayInvoice(*swap.Invoice, 10000, 30, nil)
require.NoError(t, err)

stream(boltzrpc.SwapState_PENDING)
info := stream(boltzrpc.SwapState_SUCCESSFUL)
require.True(t, info.ReverseSwap.ExternalPay)

test.MineBlock()
})

}

func TestAutoSwap(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ CREATE TABLE reverseSwaps
serviceFee INT,
serviceFeePercent REAL DEFAULT 0,
onchainFee INT,
createdAt INT
createdAt INT,
externalPay BOOLEAN
);
CREATE TABLE autobudget
(
Expand Down
8 changes: 7 additions & 1 deletion database/migration.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type swapStatus struct {
status string
}

const latestSchemaVersion = 5
const latestSchemaVersion = 6

func (database *Database) migrate() error {
version, err := database.queryVersion()
Expand Down Expand Up @@ -382,6 +382,12 @@ func (database *Database) performMigration(tx *Transaction, oldVersion int) erro
if _, err := tx.Exec("ALTER TABLE swaps DROP COLUMN autoSend"); err != nil {
return err
}
case 5:
logMigration(oldVersion)

if _, err := tx.Exec("ALTER TABLE reverseSwaps ADD COLUMN externalPay BOOLEAN"); err != nil {
return err
}

case latestSchemaVersion:
logger.Info("database already at latest schema version: " + strconv.Itoa(latestSchemaVersion))
Expand Down
9 changes: 7 additions & 2 deletions database/reverse.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type ReverseSwap struct {
ServiceFee *uint64
ServiceFeePercent utils.Percentage
OnchainFee *uint64
ExternalPay bool
}

type ReverseSwapSerialized struct {
Expand Down Expand Up @@ -68,6 +69,7 @@ type ReverseSwapSerialized struct {
ServiceFee *uint64
ServiceFeePercent utils.Percentage
OnchainFee *uint64
ExternalPay bool
}

func (reverseSwap *ReverseSwap) Serialize() ReverseSwapSerialized {
Expand Down Expand Up @@ -95,6 +97,7 @@ func (reverseSwap *ReverseSwap) Serialize() ReverseSwapSerialized {
ServiceFee: reverseSwap.ServiceFee,
ServiceFeePercent: reverseSwap.ServiceFeePercent,
OnchainFee: reverseSwap.OnchainFee,
ExternalPay: reverseSwap.ExternalPay,
}
}

Expand Down Expand Up @@ -148,6 +151,7 @@ func parseReverseSwap(rows *sql.Rows) (*ReverseSwap, error) {
"serviceFeePercent": &reverseSwap.ServiceFeePercent,
"onchainFee": &onchainFee,
"createdAt": &createdAt,
"externalPay": &reverseSwap.ExternalPay,
},
)

Expand Down Expand Up @@ -259,8 +263,8 @@ const insertReverseSwapStatement = `
INSERT INTO reverseSwaps (id, fromCurrency, toCurrency, chanIds, state, error, status, acceptZeroConf, privateKey, preimage, redeemScript,
invoice, claimAddress, expectedAmount, timeoutBlockheight, lockupTransactionId,
claimTransactionId, blindingKey, isAuto, createdAt, routingFeeMsat, serviceFee,
serviceFeePercent, onchainFee, refundPubKey, swapTree)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
serviceFeePercent, onchainFee, refundPubKey, swapTree, externalPay)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`

func (database *Database) CreateReverseSwap(reverseSwap ReverseSwap) error {
Expand Down Expand Up @@ -292,6 +296,7 @@ func (database *Database) CreateReverseSwap(reverseSwap ReverseSwap) error {
reverseSwap.OnchainFee,
formatPublicKey(reverseSwap.RefundPubKey),
formatJson(reverseSwap.SwapTree.Serialize()),
reverseSwap.ExternalPay,
)
return err
}
Expand Down
5 changes: 4 additions & 1 deletion docs/grpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,8 @@ Channel creations are an optional extension to a submarine swap in the data type
| `pair` | [`Pair`](#pair) | | |
| `chan_ids` | [`string`](#string) | repeated | a list of channel ids which are allowed for paying the invoice. can be in either cln or lnd style. |
| `wallet` | [`string`](#string) | optional | wallet from which the onchain address should be generated - only considered if `address` is not set |
| `return_immediately` | [`bool`](#bool) | | Whether the daemon should return immediately after creating the swap or wait until the swap is successful or failed. It will always return immediately if `accept_zero_conf` is not set. |
| `return_immediately` | [`bool`](#bool) | optional | Whether the daemon should return immediately after creating the swap or wait until the swap is successful or failed. It will always return immediately if `accept_zero_conf` is not set. |
| `external_pay` | [`bool`](#bool) | optional | If set, the daemon will not pay the invoice of the swap and return the invoice to be paid. This implicitly sets `return_immediately` to true. |



Expand All @@ -368,6 +369,7 @@ Channel creations are an optional extension to a submarine swap in the data type
| `lockup_address` | [`string`](#string) | | |
| `routing_fee_milli_sat` | [`uint64`](#uint64) | optional | Only populated when zero-conf is accepted and return_immediately is set to false |
| `claim_transaction_id` | [`string`](#string) | optional | Only populated when zero-conf is accepted and return_immediately is set to false |
| `invoice` | [`string`](#string) | optional | Invoice to be paid. Only populated when `external_pay` is set to true |



Expand Down Expand Up @@ -845,6 +847,7 @@ Reverse Pair
| `service_fee` | [`uint64`](#uint64) | optional | |
| `onchain_fee` | [`uint64`](#uint64) | optional | |
| `routing_fee_msat` | [`uint64`](#uint64) | optional | |
| `external_pay` | [`bool`](#bool) | | |



Expand Down
31 changes: 23 additions & 8 deletions rpcserver/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,16 @@ func (server *routedBoltzServer) CreateSwap(_ context.Context, request *boltzrpc
func (server *routedBoltzServer) createReverseSwap(isAuto bool, request *boltzrpc.CreateReverseSwapRequest) (*boltzrpc.CreateReverseSwapResponse, error) {
logger.Info("Creating Reverse Swap for " + strconv.FormatInt(request.Amount, 10) + " satoshis")

returnImmediately := request.GetReturnImmediately()
if request.GetExternalPay() {
// only error if it was explicitly set to false, implicitly set to true otherwise
if request.ReturnImmediately != nil && !returnImmediately {
return nil, handleError(errors.New("can not wait for swap transaction when using external pay"))
} else {
returnImmediately = true
}
}

claimAddress := request.Address

pair := utils.ParsePair(request.Pair)
Expand Down Expand Up @@ -522,6 +532,7 @@ func (server *routedBoltzServer) createReverseSwap(isAuto bool, request *boltzrp
LockupTransactionId: "",
ClaimTransactionId: "",
ServiceFeePercent: utils.Percentage(reversePair.Fees.Percentage),
ExternalPay: request.GetExternalPay(),
}

for _, chanId := range request.ChanIds {
Expand Down Expand Up @@ -578,19 +589,23 @@ func (server *routedBoltzServer) createReverseSwap(isAuto bool, request *boltzrp

logger.Info("Created new Reverse Swap " + reverseSwap.Id + ": " + marshalJson(reverseSwap.Serialize()))

if err := server.nursery.PayReverseSwap(&reverseSwap); err != nil {
if dbErr := server.database.UpdateReverseSwapState(&reverseSwap, boltzrpc.SwapState_ERROR, err.Error()); dbErr != nil {
return nil, handleError(dbErr)
}
return nil, handleError(err)
}

rpcResponse := &boltzrpc.CreateReverseSwapResponse{
Id: reverseSwap.Id,
LockupAddress: response.LockupAddress,
}

if !request.GetReturnImmediately() && request.AcceptZeroConf {
if request.GetExternalPay() {
rpcResponse.Invoice = &reverseSwap.Invoice
} else {
if err := server.nursery.PayReverseSwap(&reverseSwap); err != nil {
if dbErr := server.database.UpdateReverseSwapState(&reverseSwap, boltzrpc.SwapState_ERROR, err.Error()); dbErr != nil {
return nil, handleError(dbErr)
}
return nil, handleError(err)
}
}

if !returnImmediately && request.AcceptZeroConf {
updates, stop := server.nursery.SwapUpdates(reverseSwap.Id)
defer stop()

Expand Down
1 change: 1 addition & 0 deletions rpcserver/serializer.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ func serializeReverseSwap(reverseSwap *database.ReverseSwap) *boltzrpc.ReverseSw
ServiceFee: serializedReverseSwap.ServiceFee,
OnchainFee: serializedReverseSwap.OnchainFee,
RoutingFeeMsat: serializedReverseSwap.RoutingFeeMsat,
ExternalPay: serializedReverseSwap.ExternalPay,
}
}

Expand Down

0 comments on commit d23447c

Please sign in to comment.