Skip to content

feat: add swaps table #1419

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 18 commits into
base: feat/swaps
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 55 additions & 8 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,10 @@ func (api *api) GetNodeConnectionInfo(ctx context.Context) (*lnclient.NodeConnec
return api.svc.GetLNClient().GetNodeConnectionInfo(ctx)
}

func (api *api) ProcessSwapRefund(swapId string) error {
return api.svc.GetSwapsService().ProcessRefund(swapId)
}

func (api *api) GetAutoSwapConfig() (*GetAutoSwapConfigResponse, error) {
swapOutBalanceThresholdStr, _ := api.cfg.Get(config.AutoSwapBalanceThresholdKey, "")
swapOutAmountStr, _ := api.cfg.Get(config.AutoSwapAmountKey, "")
Expand All @@ -571,6 +575,51 @@ func (api *api) GetAutoSwapConfig() (*GetAutoSwapConfigResponse, error) {
}, nil
}

func (api *api) LookupSwap(swapId string) (*LookupSwapResponse, error) {
dbSwap, err := api.svc.GetSwapsService().GetSwap(swapId)
if err != nil {
logger.Logger.WithError(err).Error("failed to fetch swap info")
return nil, err
}

return toApiSwap(dbSwap), nil
}

func (api *api) ListSwaps() (*ListSwapsResponse, error) {
swaps, err := api.svc.GetSwapsService().ListSwaps()
if err != nil {
return nil, err
}

apiSwaps := []Swap{}
for _, swap := range swaps {
apiSwaps = append(apiSwaps, *toApiSwap(&swap))
}

return &ListSwapsResponse{
Swaps: apiSwaps,
}, nil
}

func toApiSwap(swap *swaps.Swap) *Swap {
return &Swap{
Id: swap.SwapId,
Type: swap.Type,
State: swap.State,
Address: swap.Address,
AmountSent: swap.AmountSent,
AmountReceived: swap.AmountReceived,
PaymentHash: swap.PaymentHash,
Destination: swap.Destination,
LockupTxId: swap.LockupTxId,
ClaimTxId: swap.ClaimTxId,
AutoSwap: swap.AutoSwap,
BoltzPubkey: swap.BoltzPubkey,
CreatedAt: swap.CreatedAt.Format(time.RFC3339),
UpdatedAt: swap.UpdatedAt.Format(time.RFC3339),
}
}

func (api *api) GetSwapInFees() (*SwapFeesResponse, error) {
swapInFees, err := api.svc.GetSwapsService().CalculateSwapInFee()
if err != nil {
Expand Down Expand Up @@ -599,7 +648,7 @@ func (api *api) GetSwapOutFees() (*SwapFeesResponse, error) {
}, nil
}

func (api *api) InitiateSwapOut(ctx context.Context, initiateSwapOutRequest *InitiateSwapRequest) (*swaps.SwapOutResponse, error) {
func (api *api) InitiateSwapOut(ctx context.Context, initiateSwapOutRequest *InitiateSwapRequest) (*swaps.SwapResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, errors.New("LNClient not started")
Expand All @@ -612,8 +661,7 @@ func (api *api) InitiateSwapOut(ctx context.Context, initiateSwapOutRequest *Ini
return nil, errors.New("invalid swap amount")
}

// TODO: Do not use context.Background - use background context in the SwapOut goroutine instead
swapoutResponse, err := api.svc.GetSwapsService().SwapOut(context.Background(), amount, destination, lnClient)
swapoutResponse, err := api.svc.GetSwapsService().SwapOut(amount, destination, false)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
Expand All @@ -625,7 +673,7 @@ func (api *api) InitiateSwapOut(ctx context.Context, initiateSwapOutRequest *Ini
return swapoutResponse, nil
}

func (api *api) InitiateSwapIn(ctx context.Context, initiateSwapInRequest *InitiateSwapRequest) (*swaps.SwapInResponse, error) {
func (api *api) InitiateSwapIn(ctx context.Context, initiateSwapInRequest *InitiateSwapRequest) (*swaps.SwapResponse, error) {
lnClient := api.svc.GetLNClient()
if lnClient == nil {
return nil, errors.New("LNClient not started")
Expand All @@ -637,8 +685,7 @@ func (api *api) InitiateSwapIn(ctx context.Context, initiateSwapInRequest *Initi
return nil, errors.New("invalid swap amount")
}

// TODO: Do not use context.Background - use background context in the SwapIn goroutine instead
txId, err := api.svc.GetSwapsService().SwapIn(context.Background(), amount, lnClient)
txId, err := api.svc.GetSwapsService().SwapIn(amount, false)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"amount": amount,
Expand Down Expand Up @@ -668,7 +715,7 @@ func (api *api) EnableAutoSwapOut(ctx context.Context, enableAutoSwapsRequest *E
return err
}

return api.svc.StartAutoSwap()
return api.svc.GetSwapsService().EnableAutoSwapOut()
}

func (api *api) DisableAutoSwap() error {
Expand All @@ -681,7 +728,7 @@ func (api *api) DisableAutoSwap() error {
}
}

api.svc.GetSwapsService().StopAutoSwap()
api.svc.GetSwapsService().StopAutoSwapOut()
return nil
}

Expand Down
30 changes: 28 additions & 2 deletions api/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ type API interface {
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
Health(ctx context.Context) (*HealthResponse, error)
SetCurrency(currency string) error
ProcessSwapRefund(swapId string) error
LookupSwap(swapId string) (*LookupSwapResponse, error)
ListSwaps() (*ListSwapsResponse, error)
GetSwapInFees() (*SwapFeesResponse, error)
GetSwapOutFees() (*SwapFeesResponse, error)
InitiateSwapIn(ctx context.Context, initiateSwapInRequest *InitiateSwapRequest) (*swaps.SwapInResponse, error)
InitiateSwapOut(ctx context.Context, initiateSwapOutRequest *InitiateSwapRequest) (*swaps.SwapOutResponse, error)
InitiateSwapIn(ctx context.Context, initiateSwapInRequest *InitiateSwapRequest) (*swaps.SwapResponse, error)
InitiateSwapOut(ctx context.Context, initiateSwapOutRequest *InitiateSwapRequest) (*swaps.SwapResponse, error)
GetAutoSwapConfig() (*GetAutoSwapConfigResponse, error)
EnableAutoSwapOut(ctx context.Context, autoSwapRequest *EnableAutoSwapRequest) error
DisableAutoSwap() error
Expand Down Expand Up @@ -149,6 +152,29 @@ type SwapFeesResponse struct {
BoltzNetworkFee uint64 `json:"boltzNetworkFee"`
}

type ListSwapsResponse struct {
Swaps []Swap `json:"swaps"`
}

type LookupSwapResponse = Swap

type Swap struct {
Id string `json:"id"`
Type string `json:"type"`
State string `json:"state"`
Address string `json:"address"`
AmountSent uint64 `json:"amountSent"`
AmountReceived uint64 `json:"amountReceived"`
PaymentHash string `json:"paymentHash"`
Destination string `json:"destination"`
LockupTxId string `json:"lockupTxId"`
ClaimTxId string `json:"claimTxId"`
AutoSwap bool `json:"autoSwap"`
BoltzPubkey string `json:"boltzPubkey"`
CreatedAt string `json:"createdAt"`
UpdatedAt string `json:"updatedAt"`
}

type StartRequest struct {
UnlockPassword string `json:"unlockPassword"`
}
Expand Down
1 change: 1 addition & 0 deletions cmd/db_migrate/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ var expectedTables = []string{
"request_events",
"response_events",
"transactions",
"swaps",
"user_configs",
"migrations",
}
Expand Down
13 changes: 10 additions & 3 deletions constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ const (
TRANSACTION_TYPE_INCOMING = "incoming"
TRANSACTION_TYPE_OUTGOING = "outgoing"

TRANSACTION_STATE_PENDING = "PENDING"
TRANSACTION_STATE_SETTLED = "SETTLED"
TRANSACTION_STATE_FAILED = "FAILED"
TRANSACTION_STATE_PENDING = "PENDING"
TRANSACTION_STATE_SETTLED = "SETTLED"
TRANSACTION_STATE_FAILED = "FAILED"
TRANSACTION_STATE_ACCEPTED = "ACCEPTED"

SWAP_TYPE_IN = "in"
SWAP_TYPE_OUT = "out"

SWAP_STATE_PENDING = "PENDING"
SWAP_STATE_SUCCESS = "SUCCESS"
SWAP_STATE_FAILED = "FAILED"
)

const (
Expand Down
47 changes: 47 additions & 0 deletions db/migrations/202506170342_swaps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package migrations

import (
_ "embed"
"text/template"

"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)

const swapsMigration = `
CREATE TABLE swaps(
id {{ .AutoincrementPrimaryKey }},
swap_id text UNIQUE,
type text,
state text,
address text,
amount_sent integer,
amount_received integer,
boltz_pubkey text,
payment_hash text,
destination text,
lockup_tx_id text,
claim_tx_id text,
auto_swap boolean,
swap_tree json,
created_at {{ .Timestamp }},
updated_at {{ .Timestamp }}
);
`

var swapsMigrationTmpl = template.Must(template.New("swapsMigration").Parse(swapsMigration))

var _202506170342_swaps = &gormigrate.Migration{
ID: "202506170342_swaps",
Migrate: func(tx *gorm.DB) error {

if err := exec(tx, swapsMigrationTmpl); err != nil {
return err
}

return nil
},
Rollback: func(tx *gorm.DB) error {
return nil
},
}
1 change: 1 addition & 0 deletions db/migrations/migrate.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func Migrate(gormDB *gorm.DB) error {
_202412212345_fix_types,
_202504231037_add_indexes,
_202505091314_hold_invoices,
_202506170342_swaps,
})

return m.Migrate()
Expand Down
19 changes: 19 additions & 0 deletions db/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ type Transaction struct {
SettleDeadline *uint32 // block number for accepted hold invoices
}

type Swap struct {
ID uint
SwapId string `validate:"required"`
Type string
State string
Address string
AmountSent uint64
AmountReceived uint64
PaymentHash string
Destination string
LockupTxId string
ClaimTxId string
AutoSwap bool
BoltzPubkey string
SwapTree datatypes.JSON
CreatedAt time.Time
UpdatedAt time.Time
}

const (
REQUEST_EVENT_STATE_HANDLER_EXECUTING = "executing"
REQUEST_EVENT_STATE_HANDLER_EXECUTED = "executed"
Expand Down
Binary file added frontend/src/assets/suggested-apps/boltz.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions frontend/src/components/TransactionItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { nip19 } from "nostr-tools";
import React from "react";
import { Link } from "react-router-dom";
import boltz from "src/assets/suggested-apps/boltz.png";
import AppAvatar from "src/components/AppAvatar";
import ExternalLink from "src/components/ExternalLink";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
Expand Down Expand Up @@ -90,6 +91,8 @@ function TransactionItem({ tx }: Props) {

const bolt12Offer = tx.metadata?.offer;

const swapId = tx.metadata?.swap_id;

const description =
tx.description || tx.metadata?.comment || bolt12Offer?.payer_note;

Expand Down Expand Up @@ -135,6 +138,13 @@ function TransactionItem({ tx }: Props) {
/>
</div>
)}
{swapId && (
<div className="absolute -bottom-1 -right-1">
<div className="w-[18px] h-[18px] md:w-6 md:h-6 shadow-sm">
<img src={boltz} className="rounded-full" />
</div>
</div>
)}
</div>
</div>
);
Expand Down Expand Up @@ -228,6 +238,17 @@ function TransactionItem({ tx }: Props) {
</Link>
</div>
)}
{swapId && (
<div className="mt-8">
<p>Boltz Swap Id</p>
<Link
to={`/wallet/swap/status/${swapId}`}
className="flex items-center gap-1"
>
<p className="underline">{swapId}</p>
</Link>
</div>
)}
{recipientIdentifier && (
<div className="mt-6">
<p>To</p>
Expand Down
20 changes: 16 additions & 4 deletions frontend/src/hooks/useSwaps.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import useSWR from "swr";
import useSWR, { SWRConfiguration } from "swr";

import { AutoSwapsConfig, SwapFees } from "src/types";
import { AutoSwapConfig, Swap, SwapFees } from "src/types";
import { swrFetcher } from "src/utils/swr";

export function useAutoSwapsConfig() {
return useSWR<AutoSwapsConfig>("/api/wallet/autoswap", swrFetcher);
return useSWR<AutoSwapConfig>("/api/autoswap", swrFetcher);
}

export function useSwapFees(direction: "in" | "out") {
return useSWR<SwapFees>(`/api/wallet/swap/${direction}/fees`, swrFetcher);
return useSWR<SwapFees>(`/api/swaps/${direction}/fees`, swrFetcher);
}

const pollConfiguration: SWRConfiguration = {
refreshInterval: 3000,
};

export function useSwap(swapId: string, poll = false) {
return useSWR<Swap>(
`/api/swaps/${swapId}`,
swrFetcher,
poll ? pollConfiguration : undefined
);
}
15 changes: 5 additions & 10 deletions frontend/src/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,7 @@ import ZeroAmount from "src/screens/wallet/send/ZeroAmount";
import Swap from "src/screens/wallet/swap";
import AutoSwap from "src/screens/wallet/swap/AutoSwap";
import AutoSwapSuccess from "src/screens/wallet/swap/AutoSwapSuccess";
import SwapInStatus from "src/screens/wallet/swap/SwapInStatus";
import SwapOutStatus from "src/screens/wallet/swap/SwapOutStatus";
import SwapStatus from "src/screens/wallet/swap/SwapStatus";

const routes = [
{
Expand Down Expand Up @@ -127,16 +126,12 @@ const routes = [
element: <Swap />,
},
{
path: "auto",
element: <AutoSwap />,
},
{
path: "in/status",
element: <SwapInStatus />,
path: "status/:swapId",
element: <SwapStatus />,
},
{
path: "out/status",
element: <SwapOutStatus />,
path: "auto",
element: <AutoSwap />,
},
{
path: "auto/success",
Expand Down
Loading