Skip to content

Commit

Permalink
feat: initial chainswaps (#133)
Browse files Browse the repository at this point in the history
* feat: initial chainswaps

* chore: cleanup

* refactor: unify fee and transaction logic

* refactor: use IN when querying refundable swaps

* refactor: use getters and setters for refund and claim pubkey in swaptree

* chore: api naming

* chore: cleanup

* feat: improve wallet logic when creating chain swap

* feat: manual chain swap refunds

* feat: include chainswaps in `ListSwaps` rpc

* chore: consistent naming

* Update nursery/refund.go

Co-authored-by: michael1011 <me@michael1011.at>

* chore: improve proto definitions

* refactor: dont set externalPay implicitly

* chore: cleanup

* refactor: use wallet ids in create request

* feat: only query coop refundable swaps in specific states

* fix: check if swap already has been paid in nursery

* chore: improve error messages

* feat: chain swaps cli (#136)

* feat: chain swap cli

* fix: chain swap help description

* fix: help output missing , statement

* fix: chain swap help description from/to

* fix: createswap,createreverseswap,createchainswap help description

* fix: autoswapper -> autoswap in help outputs, comments

* fix: typo

* refactor: dont implicitly select wallets

* fix: check for state in timeout condition when querying refundable swaps

* fix: only set fees of valid outputs after transaction was created

* feat: implicitly set external pay in cli when not specifying from wallet

* chore: cleanup

* fix: check if value is less than fee when constructing transaction

* feat: return output specific errors when creating transactions

* fix: refund help description

* fix: --from-external, --to-address help output

* refactor: dont default to mainchain when creating swap

* refactor: use from and to wallet terminology everywhere

* chore: add db migration

* fix: display correct refund transaction when refunding in cli

* fix: show correct timeout in cli

* feat: recover pending chain swaps

* fix: correctly set external pay when creating submarine swap

* feat: display currency of lockup tx in cli

* fix: dont try to broadcast empty transaction

---------

Co-authored-by: Kilian <19181985+kilrau@users.noreply.github.com>

* docs: improve grpc comments

* fix: transaction reconstruction logic

* ci: temporarily use chainswaps branch of regtest

* text: wait with mining blocks

* test: remove flaky batch test for now

can be added back later when batching has been refactored

---------

Co-authored-by: michael1011 <me@michael1011.at>
Co-authored-by: Kilian <19181985+kilrau@users.noreply.github.com>
  • Loading branch information
3 people committed Apr 27, 2024
1 parent 3a74420 commit e85d88a
Show file tree
Hide file tree
Showing 34 changed files with 4,437 additions and 1,258 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:

- name: Setup Regtest
run: |
git clone https://github.com/BoltzExchange/legend-regtest-enviroment.git ~/regtest
git clone https://github.com/BoltzExchange/legend-regtest-enviroment.git ~/regtest -b chainswaps
sudo chmod -R 777 ~/regtest
cd ~/regtest
chmod +x ./regtest
Expand Down
176 changes: 165 additions & 11 deletions boltz/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,11 @@ type SwapType string
const (
NormalSwap SwapType = "submarine"
ReverseSwap SwapType = "reverse"
ChainSwap SwapType = "chain"
)

var ErrPartialSignaturesDisabled = errors.New("partial signatures are disabled")

func ParseSwapType(swapType string) (SwapType, error) {
switch strings.ToLower(swapType) {
case string(NormalSwap), "normal":
Expand Down Expand Up @@ -91,6 +94,28 @@ type ReversePair struct {

type ReversePairs map[Currency]map[Currency]ReversePair

type ChainPair struct {
Hash string `json:"hash"`
Rate float64 `json:"rate"`
Limits struct {
Minimal uint64 `json:"minimal"`
Maximal uint64 `json:"maximal"`
MaximalZeroConfAmount uint64 `json:"maximalZeroConfAmount"`
} `json:"limits"`
Fees struct {
Percentage float64 `json:"percentage"`
MinerFees struct {
Server uint64 `json:"server"`
User struct {
Claim uint64 `json:"claim"`
Lockup uint64 `json:"lockup"`
} `json:"user"`
} `json:"minerFees"`
} `json:"fees"`
}

type ChainPairs map[Currency]map[Currency]ChainPair

type symbolMinerFees struct {
Normal uint64 `json:"normal"`
Reverse struct {
Expand Down Expand Up @@ -147,6 +172,20 @@ type GetSwapTransactionResponse struct {
Error string `json:"error"`
}

type ChainSwapTransaction struct {
Transaction struct {
Id string `json:"id"`
Hex string `json:"hex"`
} `json:"transaction"`
}

type GetChainSwapTransactionsResponse struct {
UserLock *ChainSwapTransaction `json:"userLock"`
ServerLock *ChainSwapTransaction `json:"serverLock"`

Error string `json:"error"`
}

type GetTransactionRequest struct {
Currency string `json:"currency"`
TransactionId string `json:"transactionId"`
Expand Down Expand Up @@ -197,8 +236,7 @@ type CreateSwapResponse struct {
Error string `json:"error"`
}

type RefundSwapRequest struct {
Id string `json:"id"`
type RefundRequest struct {
PubNonce HexString `json:"pubNonce"`
Transaction string `json:"transaction"`
Index int `json:"index"`
Expand All @@ -213,6 +251,22 @@ type SwapClaimDetails struct {
Error string `json:"error"`
}

type ChainSwapSigningDetails struct {
PubNonce HexString `json:"pubNonce"`
TransactionHash HexString `json:"transactionHash"`
PublicKey HexString `json:"publicKey"`

Error string `json:"error"`
}

type ChainSwapSigningRequest struct {
Preimage HexString `json:"preimage"`
Signature *PartialSignature `json:"signature"`
ToSign *ClaimRequest `json:"toSign"`

Error string `json:"error"`
}

type GetInvoiceAmountResponse struct {
InvoiceAmount uint64 `json:"invoiceAmount"`
Error string `json:"error"`
Expand Down Expand Up @@ -251,14 +305,43 @@ type CreateReverseSwapResponse struct {

Error string `json:"error"`
}
type ClaimReverseSwapRequest struct {
Id string `json:"id"`
type ClaimRequest struct {
Preimage HexString `json:"preimage"`
PubNonce HexString `json:"pubNonce"`
Transaction string `json:"transaction"`
Index int `json:"index"`
}

type ChainRequest struct {
From Currency `json:"from"`
To Currency `json:"to"`
PreimageHash HexString `json:"preimageHash"`
ClaimPublicKey HexString `json:"claimPublicKey,omitempty"`
RefundPublicKey HexString `json:"refundPublicKey,omitempty"`
UserLockAmount uint64 `json:"userLockAmount,omitempty"`
ServerLockAmount uint64 `json:"serverLockAmount,omitempty"`
PairHash string `json:"pairHash,omitempty"`
ReferralId string `json:"referralId,omitempty"`
}

type ChainResponse struct {
Id string `json:"id"`
ClaimDetails *ChainSwapData `json:"claimDetails,omitempty"`
LockupDetails *ChainSwapData `json:"lockupDetails,omitempty"`

Error string `json:"error"`
}

type ChainSwapData struct {
SwapTree *SerializedTree `json:"swapTree,omitempty"`
LockupAddress string `json:"lockupAddress"`
ServerPublicKey HexString `json:"serverPublicKey,omitempty"`
TimeoutBlockHeight uint32 `json:"timeoutBlockHeight"`
Amount uint64 `json:"amount"`
BlindingKey HexString `json:"blindingKey,omitempty"`
Bip21 string `json:"bip21,omitempty"`
}

type PartialSignature struct {
PubNonce HexString `json:"pubNonce"`
PartialSignature HexString `json:"partialSignature"`
Expand Down Expand Up @@ -303,6 +386,12 @@ func (boltz *Boltz) GetReversePairs() (response ReversePairs, err error) {
return response, err
}

func (boltz *Boltz) GetChainPairs() (response ChainPairs, err error) {
err = boltz.sendGetRequest("/v2/swap/chain", &response)

return response, err
}

func (boltz *Boltz) GetNodes() (Nodes, error) {
var response Nodes
err := boltz.sendGetRequest("/v2/nodes", &response)
Expand Down Expand Up @@ -334,6 +423,18 @@ func (boltz *Boltz) GetSwapTransaction(id string) (*GetSwapTransactionResponse,
return &response, err
}

func (boltz *Boltz) GetChainSwapTransactions(id string) (*GetChainSwapTransactionsResponse, error) {
var response GetChainSwapTransactionsResponse
path := fmt.Sprintf("/v2/swap/chain/%s/transactions", id)
err := boltz.sendGetRequest(path, &response)

if response.Error != "" {
return nil, Error(errors.New(response.Error))
}

return &response, err
}

func (boltz *Boltz) GetTransaction(transactionId string, currency Currency) (string, error) {
var response GetTransactionResponse
path := fmt.Sprintf("/v2/chain/%s/transaction/%s", currency, transactionId)
Expand Down Expand Up @@ -379,12 +480,12 @@ func (boltz *Boltz) CreateSwap(request CreateSwapRequest) (*CreateSwapResponse,
return &response, err
}

func (boltz *Boltz) RefundSwap(request RefundSwapRequest) (*PartialSignature, error) {
func (boltz *Boltz) RefundSwap(swapId string, request *RefundRequest) (*PartialSignature, error) {
if boltz.DisablePartialSignatures {
return nil, errors.New("partial signatures are disabled")
return nil, ErrPartialSignaturesDisabled
}
var response PartialSignature
err := boltz.sendPostRequest("/v2/swap/submarine/refund", request, &response)
err := boltz.sendPostRequest(fmt.Sprintf("/v2/swap/submarine/%s/refund", swapId), request, &response)

if response.Error != "" {
return nil, Error(errors.New(response.Error))
Expand All @@ -406,7 +507,7 @@ func (boltz *Boltz) GetInvoiceAmount(swapId string) (*GetInvoiceAmountResponse,

func (boltz *Boltz) GetSwapClaimDetails(swapId string) (*SwapClaimDetails, error) {
if boltz.DisablePartialSignatures {
return nil, errors.New("partial signatures are disabled")
return nil, ErrPartialSignaturesDisabled
}
var response SwapClaimDetails
err := boltz.sendGetRequest(fmt.Sprintf("/v2/swap/submarine/%s/claim", swapId), &response)
Expand Down Expand Up @@ -451,12 +552,65 @@ func (boltz *Boltz) CreateReverseSwap(request CreateReverseSwapRequest) (*Create
return &response, err
}

func (boltz *Boltz) ClaimReverseSwap(request ClaimReverseSwapRequest) (*PartialSignature, error) {
func (boltz *Boltz) ClaimReverseSwap(swapId string, request *ClaimRequest) (*PartialSignature, error) {
if boltz.DisablePartialSignatures {
return nil, ErrPartialSignaturesDisabled
}
var response PartialSignature
err := boltz.sendPostRequest(fmt.Sprintf("/v2/swap/reverse/%s/claim", swapId), request, &response)

if response.Error != "" {
return nil, Error(errors.New(response.Error))
}

return &response, err
}

func (boltz *Boltz) CreateChainSwap(request ChainRequest) (*ChainResponse, error) {
var response ChainResponse
err := boltz.sendPostRequest("/v2/swap/chain", request, &response)

if response.Error != "" {
return nil, Error(errors.New(response.Error))
}

return &response, err
}

func (boltz *Boltz) GetChainSwapClaimDetails(swapId string) (*ChainSwapSigningDetails, error) {
if boltz.DisablePartialSignatures {
return nil, ErrPartialSignaturesDisabled
}
var response ChainSwapSigningDetails
err := boltz.sendGetRequest(fmt.Sprintf("/v2/swap/chain/%s/claim", swapId), &response)

if response.Error != "" {
return nil, Error(errors.New(response.Error))
}

return &response, err
}

func (boltz *Boltz) ExchangeChainSwapClaimSignature(swapId string, request *ChainSwapSigningRequest) (*PartialSignature, error) {
if boltz.DisablePartialSignatures {
return nil, ErrPartialSignaturesDisabled
}
var response PartialSignature
err := boltz.sendPostRequest(fmt.Sprintf("/v2/swap/chain/%s/claim", swapId), request, &response)

if response.Error != "" {
return nil, Error(errors.New(response.Error))
}

return &response, err
}

func (boltz *Boltz) RefundChainSwap(swapId string, request *RefundRequest) (*PartialSignature, error) {
if boltz.DisablePartialSignatures {
return nil, errors.New("partial signatures are disabled")
return nil, ErrPartialSignaturesDisabled
}
var response PartialSignature
err := boltz.sendPostRequest("/v2/swap/reverse/claim", request, &response)
err := boltz.sendPostRequest(fmt.Sprintf("/v2/swap/chain/%s/refund", swapId), request, &response)

if response.Error != "" {
return nil, Error(errors.New(response.Error))
Expand Down
23 changes: 6 additions & 17 deletions boltz/btc.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ func (transaction *BtcTransaction) FindVout(network *Network, addressToFind stri
return 0, 0, errors.New("Could not find address in transaction")
}

func (transaction *BtcTransaction) VoutValue(vout uint32) (uint64, error) {
return uint64(transaction.MsgTx().TxOut[vout].Value), nil
}

func getPrevoutFetcher(tx *wire.MsgTx, outputs []OutputDetails) txscript.PrevOutputFetcher {
previous := make(map[wire.OutPoint]*wire.TxOut)
for i, input := range tx.TxIn {
Expand Down Expand Up @@ -99,11 +103,9 @@ func btcTaprootHash(transaction Transaction, outputs []OutputDetails, index int)
)
}

func constructBtcTransaction(network *Network, outputs []OutputDetails, fee uint64) (Transaction, error) {
func constructBtcTransaction(network *Network, outputs []OutputDetails, outValues map[string]uint64) (Transaction, error) {
transaction := wire.NewMsgTx(wire.TxVersion)

outValues := make(map[string]int64)

for _, output := range outputs {
// Set the highest timeout block height as locktime
if !output.Cooperative {
Expand All @@ -119,18 +121,8 @@ func constructBtcTransaction(network *Network, outputs []OutputDetails, fee uint
input.Sequence = 0

transaction.AddTxIn(input)

value := lockupTx.MsgTx().TxOut[output.Vout].Value
//nolint:gosimple
existingValue, _ := outValues[output.Address]
outValues[output.Address] = existingValue + value

}

outLen := uint64(len(outValues))
feePerOutput := fee / outLen
feeRemainder := fee % outLen

for rawAddress, value := range outValues {
outputAddress, err := btcutil.DecodeAddress(rawAddress, network.Btc)
if err != nil {
Expand All @@ -143,12 +135,9 @@ func constructBtcTransaction(network *Network, outputs []OutputDetails, fee uint
return nil, err
}

// give the remainder to the first output
fee := feePerOutput + feeRemainder
feeRemainder = 0
transaction.AddTxOut(&wire.TxOut{
PkScript: outputScript,
Value: value - int64(fee),
Value: int64(value),
})
}

Expand Down
19 changes: 12 additions & 7 deletions boltz/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ const (
TransactionConfirmed
TransactionLockupFailed
TransactionClaimPending

TransactionServerMempoool
TransactionServerConfirmed
)

var swapUpdateEventStrings = map[string]SwapUpdateEvent{
Expand All @@ -35,13 +38,15 @@ var swapUpdateEventStrings = map[string]SwapUpdateEvent{

"channel.created": ChannelCreated,

"transaction.failed": TransactionFailed,
"transaction.mempool": TransactionMempool,
"transaction.claimed": TransactionClaimed,
"transaction.refunded": TransactionRefunded,
"transaction.confirmed": TransactionConfirmed,
"transaction.lockupFailed": TransactionLockupFailed,
"transaction.claim.pending": TransactionClaimPending,
"transaction.failed": TransactionFailed,
"transaction.mempool": TransactionMempool,
"transaction.claimed": TransactionClaimed,
"transaction.refunded": TransactionRefunded,
"transaction.confirmed": TransactionConfirmed,
"transaction.lockupFailed": TransactionLockupFailed,
"transaction.claim.pending": TransactionClaimPending,
"transaction.server.mempool": TransactionServerMempoool,
"transaction.server.confirmed": TransactionServerConfirmed,
}

var CompletedStatus = []string{
Expand Down
Loading

0 comments on commit e85d88a

Please sign in to comment.