From 7a32bfaeccd6ac7e1d3630b17ec361fcbb0a203c Mon Sep 17 00:00:00 2001 From: Nikolas De Giorgis Date: Mon, 8 Dec 2025 13:18:29 +0100 Subject: [PATCH 1/4] backend/swap: add implementation of swap quote. --- backend/handlers/handlers.go | 36 ++++++++++++++++ backend/market/swapkit/client.go | 65 +++++++++++++++++++++++++++++ backend/market/swapkit/methods.go | 14 +++++++ backend/market/swapkit/types.go | 69 +++++++++++++++++++++++++++++++ 4 files changed, 184 insertions(+) create mode 100644 backend/market/swapkit/client.go create mode 100644 backend/market/swapkit/methods.go create mode 100644 backend/market/swapkit/types.go diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 809dbc15ff..eb733ec519 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -17,6 +17,7 @@ package handlers import ( "bytes" + "context" "encoding/base64" "encoding/hex" "encoding/json" @@ -49,6 +50,7 @@ import ( "github.com/BitBoxSwiss/bitbox-wallet-app/backend/devices/device" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/keystore" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/market" + "github.com/BitBoxSwiss/bitbox-wallet-app/backend/market/swapkit" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/rates" "github.com/BitBoxSwiss/bitbox-wallet-app/backend/versioninfo" utilConfig "github.com/BitBoxSwiss/bitbox-wallet-app/util/config" @@ -247,6 +249,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/market/vendors/{code}", handlers.getMarketVendors).Methods("GET") getAPIRouterNoError(apiRouter)("/market/btcdirect-otc/supported/{code}", handlers.getMarketBtcDirectOTCSupported).Methods("GET") getAPIRouterNoError(apiRouter)("/market/btcdirect/info/{action}/{code}", handlers.getMarketBtcDirectInfo).Methods("GET") + getAPIRouterNoError(apiRouter)("/swap/quote", handlers.getSwapkitQuote).Methods("GET") getAPIRouter(apiRouter)("/market/moonpay/buy-info/{code}", handlers.getMarketMoonpayBuyInfo).Methods("GET") getAPIRouterNoError(apiRouter)("/market/pocket/api-url/{action}", handlers.getMarketPocketURL).Methods("GET") getAPIRouterNoError(apiRouter)("/market/pocket/verify-address", handlers.postPocketWidgetVerifyAddress).Methods("POST") @@ -1664,3 +1667,36 @@ func (handlers *Handlers) postConnectKeystore(r *http.Request) interface{} { _, err := handlers.backend.ConnectKeystore([]byte(request.RootFingerprint)) return response{Success: err == nil} } + +func (handlers *Handlers) getSwapkitQuote(r *http.Request) interface{} { + type result struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + Quote *swapkit.QuoteResponse `json:"quote,omitempty"` + } + + var request swapkit.QuoteRequest + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + return result{Success: false} + } + + s := swapkit.NewClient("0722e09f-9d3f-4817-a870-069848d03ee9") + + quoteResponse, err := s.Quote(context.Background(), &request) + if err != nil { + return result{ + Success: false, + Error: err.Error(), + } + } + + res := result{ + Success: quoteResponse.Error != "", + Error: quoteResponse.Error, // Surface the response error to the top-level + } + if res.Success { + res.Quote = quoteResponse + } + return res +} diff --git a/backend/market/swapkit/client.go b/backend/market/swapkit/client.go new file mode 100644 index 0000000000..a4e324754e --- /dev/null +++ b/backend/market/swapkit/client.go @@ -0,0 +1,65 @@ +package swapkit + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "time" +) + +type Client struct { + apiKey string + baseURL string + httpClient *http.Client +} + +func NewClient(apiKey string) *Client { + return &Client{ + apiKey: apiKey, + baseURL: "https://api.swapkit.dev/v3", + httpClient: &http.Client{ + Timeout: 20 * time.Second, + }, + } +} + +func (c *Client) post(ctx context.Context, path string, body any, out any) error { + b, err := json.Marshal(body) + if err != nil { + return fmt.Errorf("marshal request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+path, bytes.NewReader(b)) + if err != nil { + return fmt.Errorf("create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + if c.apiKey != "" { + req.Header.Set("x-api-key", c.apiKey) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("http error: %w", err) + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("read response: %w", err) + } + + if resp.StatusCode >= 400 { + return fmt.Errorf("swapkit error %d: %s", resp.StatusCode, string(bodyBytes)) + } + + if err := json.Unmarshal(bodyBytes, out); err != nil { + return fmt.Errorf("decode response: %w", err) + } + + return nil +} diff --git a/backend/market/swapkit/methods.go b/backend/market/swapkit/methods.go new file mode 100644 index 0000000000..e6f92e3c15 --- /dev/null +++ b/backend/market/swapkit/methods.go @@ -0,0 +1,14 @@ +package swapkit + +import ( + "context" +) + +// Quote performs a SwapKit V3 quote request. +func (c *Client) Quote(ctx context.Context, req *QuoteRequest) (*QuoteResponse, error) { + var resp QuoteResponse + if err := c.post(ctx, "/quote", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} diff --git a/backend/market/swapkit/types.go b/backend/market/swapkit/types.go new file mode 100644 index 0000000000..96f8ae48b9 --- /dev/null +++ b/backend/market/swapkit/types.go @@ -0,0 +1,69 @@ +package swapkit + +import "encoding/json" + +type QuoteRequest struct { + SellAsset string `json:"sellAsset"` + BuyAsset string `json:"buyAsset"` + SellAmount string `json:"sellAmount"` + Providers []string `json:"providers,omitempty"` + Slippage *string `json:"slippage,omitempty"` + AffiliateFee *int `json:"affiliateFee,omitempty"` + CfBoost *bool `json:"cfBoost,omitempty"` + MaxExecutionTime *int `json:"maxExecutionTime,omitempty"` +} + +type QuoteResponse struct { + QuoteID string `json:"quoteId"` + Routes []QuoteRoute `json:"routes"` + ProviderErrors []QuoteError `json:"providerErrors,omitempty"` + Error string `json:"error,omitempty"` +} + +type QuoteRoute struct { + RouteID string `json:"routeId"` + Providers []string `json:"providers"` + SellAsset string `json:"sellAsset"` + BuyAsset string `json:"buyAsset"` + SellAmount string `json:"sellAmount"` + ExpectedBuyAmount string `json:"expectedBuyAmount"` + ExpectedBuyAmountMaxSlippage string `json:"expectedBuyAmountMaxSlippage"` + + // tx object varies by chain: + // - EVM → Ethers v6 transaction + // - UTXO → base64 PSBT + Tx json.RawMessage `json:"tx"` + + ApprovalTx json.RawMessage `json:"approvalTx,omitempty"` + + TargetAddress string `json:"targetAddress"` + Memo string `json:"memo,omitempty"` + Fees []Fee `json:"fees"` + EstimatedTime json.RawMessage `json:"estimatedTime,omitempty"` + TotalSlippageBps float64 `json:"totalSlippageBps"` + Legs json.RawMessage `json:"legs,omitempty"` + Warnings json.RawMessage `json:"warnings,omitempty"` + Meta json.RawMessage `json:"meta,omitempty"` + + NextActions []NextAction `json:"nextActions,omitempty"` +} + +type Fee struct { + Type string `json:"type"` + Amount string `json:"amount"` + Asset string `json:"asset"` + Chain string `json:"chain"` + Protocol string `json:"protocol"` +} + +type NextAction struct { + Method string `json:"method"` + URL string `json:"url"` + Payload json.RawMessage `json:"payload,omitempty"` +} + +type QuoteError struct { + Provider string `json:"provider"` + ErrorCode string `json:"errorCode"` + Message string `json:"message"` +} From 03e5f0aa1b97b57a1739cf065c33e3364bc381c3 Mon Sep 17 00:00:00 2001 From: Nikolas De Giorgis Date: Mon, 8 Dec 2025 13:43:30 +0100 Subject: [PATCH 2/4] backend/swap: implement swap endpoint. --- backend/handlers/handlers.go | 32 +++++++++++++++++++++++++++++- backend/market/swapkit/methods.go | 8 ++++++++ backend/market/swapkit/types.go | 33 +++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index eb733ec519..92f9831148 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -250,6 +250,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/market/btcdirect-otc/supported/{code}", handlers.getMarketBtcDirectOTCSupported).Methods("GET") getAPIRouterNoError(apiRouter)("/market/btcdirect/info/{action}/{code}", handlers.getMarketBtcDirectInfo).Methods("GET") getAPIRouterNoError(apiRouter)("/swap/quote", handlers.getSwapkitQuote).Methods("GET") + getAPIRouterNoError(apiRouter)("/swap/execute", handlers.swapkitSwap).Methods("GET") getAPIRouter(apiRouter)("/market/moonpay/buy-info/{code}", handlers.getMarketMoonpayBuyInfo).Methods("GET") getAPIRouterNoError(apiRouter)("/market/pocket/api-url/{action}", handlers.getMarketPocketURL).Methods("GET") getAPIRouterNoError(apiRouter)("/market/pocket/verify-address", handlers.postPocketWidgetVerifyAddress).Methods("POST") @@ -1678,7 +1679,7 @@ func (handlers *Handlers) getSwapkitQuote(r *http.Request) interface{} { var request swapkit.QuoteRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { - return result{Success: false} + return result{Success: false, Error: err.Error()} } s := swapkit.NewClient("0722e09f-9d3f-4817-a870-069848d03ee9") @@ -1700,3 +1701,32 @@ func (handlers *Handlers) getSwapkitQuote(r *http.Request) interface{} { } return res } + +func (handlers *Handlers) swapkitSwap(r *http.Request) interface{} { + type result struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + Swap swapkit.SwapResponse `json:"swap,omitempty"` + } + + var request swapkit.SwapRequest + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + return result{Success: false, Error: err.Error()} + } + + s := swapkit.NewClient("0722e09f-9d3f-4817-a870-069848d03ee9") + swapResponse, err := s.Swap(context.Background(), &request) + if err != nil { + return result{ + Success: false, + Error: err.Error(), + } + } + + return result{ + Success: true, + Swap: swapResponse, + } + +} diff --git a/backend/market/swapkit/methods.go b/backend/market/swapkit/methods.go index e6f92e3c15..a1a27f591d 100644 --- a/backend/market/swapkit/methods.go +++ b/backend/market/swapkit/methods.go @@ -12,3 +12,11 @@ func (c *Client) Quote(ctx context.Context, req *QuoteRequest) (*QuoteResponse, } return &resp, nil } + +func (c *Client) Swap(ctx context.Context, req *SwapRequest) (*SwapResponse, error) { + var resp SwapResponse + if err := c.post(ctx, "/swap", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} diff --git a/backend/market/swapkit/types.go b/backend/market/swapkit/types.go index 96f8ae48b9..a6016cc020 100644 --- a/backend/market/swapkit/types.go +++ b/backend/market/swapkit/types.go @@ -13,6 +13,18 @@ type QuoteRequest struct { MaxExecutionTime *int `json:"maxExecutionTime,omitempty"` } +type SwapRequest struct { + RouteID string `json:"routeId"` + SourceAddress string `json:"sourceAddress"` + DestinationAddress string `json:"destinationAddress"` + DisableBalanceCheck *bool `json:"disableBalanceCheck,omitempty"` + DisableEstimate *bool `json:"disableEstimate,omitempty"` + AllowSmartContractSender *bool `json:"allowSmartContractSender,omitempty"` + AllowSmartContractReceiver *bool `json:"allowSmartContractReceiver,omitempty"` + DisableSecurityChecks *bool `json:"disableSecurityChecks,omitempty"` + OverrideSlippage *bool `json:"overrideSlippage,omitempty"` +} + type QuoteResponse struct { QuoteID string `json:"quoteId"` Routes []QuoteRoute `json:"routes"` @@ -20,6 +32,27 @@ type QuoteResponse struct { Error string `json:"error,omitempty"` } +type SwapResponse struct { + RouteID string `json:"routeId"` + Providers []string `json:"providers"` + SellAsset string `json:"sellAsset"` + BuyAsset string `json:"buyAsset"` + SellAmount string `json:"sellAmount"` + ExpectedBuyAmount string `json:"expectedBuyAmount"` + ExpectedBuyAmountMaxSlippage string `json:"expectedBuyAmountMaxSlippage"` + Tx json.RawMessage `json:"tx"` + ApprovalTx json.RawMessage `json:"approvalTx,omitempty"` + TargetAddress string `json:"targetAddress"` + Memo string `json:"memo,omitempty"` + Fees []Fee `json:"fees"` + EstimatedTime json.RawMessage `json:"estimatedTime,omitempty"` + TotalSlippageBps int `json:"totalSlippageBps"` + Legs json.RawMessage `json:"legs,omitempty"` + Warnings json.RawMessage `json:"warnings,omitempty"` + Meta json.RawMessage `json:"meta,omitempty"` + NextActions []NextAction `json:"nextActions,omitempty"` +} + type QuoteRoute struct { RouteID string `json:"routeId"` Providers []string `json:"providers"` From aae8eb95482d6292e85c49b8c69eb2a14106b0d7 Mon Sep 17 00:00:00 2001 From: Nikolas De Giorgis Date: Mon, 8 Dec 2025 14:16:01 +0100 Subject: [PATCH 3/4] backend/swap: add implementation of track. --- backend/handlers/handlers.go | 36 +++++++++++++++-- backend/market/swapkit/client.go | 16 +++++++- backend/market/swapkit/methods.go | 14 ++++++- backend/market/swapkit/types.go | 67 ++++++++++++++++++++++++++++--- 4 files changed, 121 insertions(+), 12 deletions(-) diff --git a/backend/handlers/handlers.go b/backend/handlers/handlers.go index 92f9831148..2b72a950f7 100644 --- a/backend/handlers/handlers.go +++ b/backend/handlers/handlers.go @@ -251,6 +251,7 @@ func NewHandlers( getAPIRouterNoError(apiRouter)("/market/btcdirect/info/{action}/{code}", handlers.getMarketBtcDirectInfo).Methods("GET") getAPIRouterNoError(apiRouter)("/swap/quote", handlers.getSwapkitQuote).Methods("GET") getAPIRouterNoError(apiRouter)("/swap/execute", handlers.swapkitSwap).Methods("GET") + getAPIRouterNoError(apiRouter)("/swap/track", handlers.swapkitTrack).Methods("GET") getAPIRouter(apiRouter)("/market/moonpay/buy-info/{code}", handlers.getMarketMoonpayBuyInfo).Methods("GET") getAPIRouterNoError(apiRouter)("/market/pocket/api-url/{action}", handlers.getMarketPocketURL).Methods("GET") getAPIRouterNoError(apiRouter)("/market/pocket/verify-address", handlers.postPocketWidgetVerifyAddress).Methods("POST") @@ -1704,9 +1705,9 @@ func (handlers *Handlers) getSwapkitQuote(r *http.Request) interface{} { func (handlers *Handlers) swapkitSwap(r *http.Request) interface{} { type result struct { - Success bool `json:"success"` - Error string `json:"error,omitempty"` - Swap swapkit.SwapResponse `json:"swap,omitempty"` + Success bool `json:"success"` + Error string `json:"error,omitempty"` + Swap *swapkit.SwapResponse `json:"swap,omitempty"` } var request swapkit.SwapRequest @@ -1730,3 +1731,32 @@ func (handlers *Handlers) swapkitSwap(r *http.Request) interface{} { } } + +func (handlers *Handlers) swapkitTrack(r *http.Request) interface{} { + type result struct { + Success bool `json:"success"` + Error string `json:"error,omitempty"` + Track *swapkit.TrackResponse `json:"swap,omitempty"` + } + + var request swapkit.TrackRequest + + if err := json.NewDecoder(r.Body).Decode(&request); err != nil { + return result{Success: false, Error: err.Error()} + } + + s := swapkit.NewClient("0722e09f-9d3f-4817-a870-069848d03ee9") + trackResponse, err := s.Track(context.Background(), &request) + if err != nil { + return result{ + Success: false, + Error: err.Error(), + } + } + + return result{ + Success: true, + Track: trackResponse, + } + +} diff --git a/backend/market/swapkit/client.go b/backend/market/swapkit/client.go index a4e324754e..791c613b2c 100644 --- a/backend/market/swapkit/client.go +++ b/backend/market/swapkit/client.go @@ -8,21 +8,29 @@ import ( "io" "net/http" "time" + + "github.com/BitBoxSwiss/bitbox-wallet-app/util/logging" + "github.com/sirupsen/logrus" ) +// Client is a SwapKit client that can be used to interact with +// SwapKit API. type Client struct { apiKey string baseURL string httpClient *http.Client + log *logrus.Entry } +// NewClient returns a new swapkit client. func NewClient(apiKey string) *Client { return &Client{ apiKey: apiKey, - baseURL: "https://api.swapkit.dev/v3", + baseURL: "https://api.swapkit.dev", httpClient: &http.Client{ Timeout: 20 * time.Second, }, + log: logging.Get().WithGroup("swapkit"), } } @@ -46,7 +54,11 @@ func (c *Client) post(ctx context.Context, path string, body any, out any) error if err != nil { return fmt.Errorf("http error: %w", err) } - defer resp.Body.Close() + defer func() { + if err := resp.Body.Close(); err != nil { + c.log.WithError(err).Error("Error closing response body") + } + }() bodyBytes, err := io.ReadAll(resp.Body) if err != nil { diff --git a/backend/market/swapkit/methods.go b/backend/market/swapkit/methods.go index a1a27f591d..b85063741e 100644 --- a/backend/market/swapkit/methods.go +++ b/backend/market/swapkit/methods.go @@ -7,15 +7,25 @@ import ( // Quote performs a SwapKit V3 quote request. func (c *Client) Quote(ctx context.Context, req *QuoteRequest) (*QuoteResponse, error) { var resp QuoteResponse - if err := c.post(ctx, "/quote", req, &resp); err != nil { + if err := c.post(ctx, "/v3/quote", req, &resp); err != nil { return nil, err } return &resp, nil } +// Swap performs a SwapKit V3 swap request. func (c *Client) Swap(ctx context.Context, req *SwapRequest) (*SwapResponse, error) { var resp SwapResponse - if err := c.post(ctx, "/swap", req, &resp); err != nil { + if err := c.post(ctx, "/v3/swap", req, &resp); err != nil { + return nil, err + } + return &resp, nil +} + +// Track performs a SwapKit track request. +func (c *Client) Track(ctx context.Context, req *TrackRequest) (*TrackResponse, error) { + var resp TrackResponse + if err := c.post(ctx, "/track", req, &resp); err != nil { return nil, err } return &resp, nil diff --git a/backend/market/swapkit/types.go b/backend/market/swapkit/types.go index a6016cc020..205c392b05 100644 --- a/backend/market/swapkit/types.go +++ b/backend/market/swapkit/types.go @@ -2,6 +2,7 @@ package swapkit import "encoding/json" +// QuoteRequest represents a request to swapkit for a swap quote. type QuoteRequest struct { SellAsset string `json:"sellAsset"` BuyAsset string `json:"buyAsset"` @@ -13,6 +14,7 @@ type QuoteRequest struct { MaxExecutionTime *int `json:"maxExecutionTime,omitempty"` } +// SwapRequest represents a request to swakip to execute a swap. type SwapRequest struct { RouteID string `json:"routeId"` SourceAddress string `json:"sourceAddress"` @@ -25,13 +27,15 @@ type SwapRequest struct { OverrideSlippage *bool `json:"overrideSlippage,omitempty"` } +// QuoteResponse contains info about swaps' quotes. type QuoteResponse struct { - QuoteID string `json:"quoteId"` - Routes []QuoteRoute `json:"routes"` - ProviderErrors []QuoteError `json:"providerErrors,omitempty"` - Error string `json:"error,omitempty"` + QuoteID string `json:"quoteId"` + Routes []QuoteRoute `json:"routes"` + ProviderErrors []ProviderError `json:"providerErrors,omitempty"` + Error string `json:"error,omitempty"` } +// SwapResponse is the answer provided by swapkit when asking to execute a swap. type SwapResponse struct { RouteID string `json:"routeId"` Providers []string `json:"providers"` @@ -53,6 +57,8 @@ type SwapResponse struct { NextActions []NextAction `json:"nextActions,omitempty"` } +// QuoteRoute represent a single route to swap coins from +// SellAsset to BuyAsset. type QuoteRoute struct { RouteID string `json:"routeId"` Providers []string `json:"providers"` @@ -81,6 +87,7 @@ type QuoteRoute struct { NextActions []NextAction `json:"nextActions,omitempty"` } +// Fee represents one of the possible fees for executing a swap. type Fee struct { Type string `json:"type"` Amount string `json:"amount"` @@ -89,14 +96,64 @@ type Fee struct { Protocol string `json:"protocol"` } +// NextAction is provided by swap as a convenience field to suggest what +// the next step in a swap workflow could be. type NextAction struct { Method string `json:"method"` URL string `json:"url"` Payload json.RawMessage `json:"payload,omitempty"` } -type QuoteError struct { +// ProviderError contains errors specific to a Provider +// (e.g. some provided will only provide quotes for sell amounts +// higher than a certain treshold). +type ProviderError struct { Provider string `json:"provider"` ErrorCode string `json:"errorCode"` Message string `json:"message"` } + +// TrackRequest is used to query swapkit fo track the status of a swap. +type TrackRequest struct { + Hash string `json:"hash"` + ChainID string `json:"chainId"` +} + +// TrackResponse represents SwapKit's response for a tracked transaction +type TrackResponse struct { + ChainID string `json:"chainId"` + Hash string `json:"hash"` + Block int64 `json:"block"` + Type string `json:"type"` // swap, token_transfer, etc. + Status string `json:"status"` // not_started, pending, swapping, completed, refunded, failed, unknown + TrackingStatus string `json:"trackingStatus"` // deprecated, status is enough + FromAsset string `json:"fromAsset"` + FromAmount string `json:"fromAmount"` + FromAddress string `json:"fromAddress"` + ToAsset string `json:"toAsset"` + ToAmount string `json:"toAmount"` + ToAddress string `json:"toAddress"` + FinalisedAt int64 `json:"finalisedAt"` // UNIX timestamp + Meta json.RawMessage `json:"meta,omitempty"` // provider, images, etc. + Payload json.RawMessage `json:"payload,omitempty"` // transaction-specific info + Legs []TrackLeg `json:"legs,omitempty"` // individual steps in transaction +} + +// TrackLeg represents a step of the transaction +type TrackLeg struct { + ChainID string `json:"chainId"` + Hash string `json:"hash"` + Block int64 `json:"block"` + Type string `json:"type"` + Status string `json:"status"` + TrackingStatus string `json:"trackingStatus"` + FromAsset string `json:"fromAsset"` + FromAmount string `json:"fromAmount"` + FromAddress string `json:"fromAddress"` + ToAsset string `json:"toAsset"` + ToAmount string `json:"toAmount"` + ToAddress string `json:"toAddress"` + FinalisedAt int64 `json:"finalisedAt"` + Meta json.RawMessage `json:"meta,omitempty"` + Payload json.RawMessage `json:"payload,omitempty"` +} From 44cc04d17af2bf151a09a99a909ad121801936bf Mon Sep 17 00:00:00 2001 From: thisconnect Date: Thu, 4 Dec 2025 13:32:18 +0100 Subject: [PATCH 4/4] frontend: propose swap api --- frontends/web/src/api/swap.ts | 66 +++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 frontends/web/src/api/swap.ts diff --git a/frontends/web/src/api/swap.ts b/frontends/web/src/api/swap.ts new file mode 100644 index 0000000000..6621676cf7 --- /dev/null +++ b/frontends/web/src/api/swap.ts @@ -0,0 +1,66 @@ +/** + * Copyright 2025 Shift Crypto AG + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { FailResponse, SuccessResponse } from './response'; +import type { AccountCode } from './account'; +import { apiGet, apiPost } from '@/utils/request'; +import { subscribeEndpoint, TUnsubscribe } from './subscribe'; + +export type TSwapQuotes = { + quoteId: string; + buyAsset: 'ETH.ETH'; + sellAsset: 'BTC.BTC'; + sellAmount: '0.001'; + // expectedBuyAmount; + // expectedBuyAmountMaxSlippage; + // fees: []; + // routeId: string; + // ... + // expiration + // estimatedTime + // warnings: []; + // targetAddress // so we can show the address in the app so the user can confirm with the one on the device + // memo? +}; + +export const getSwapState = (): Promise => { + return apiGet('swap/state'); +}; + +export const syncSwapState = ( + cb: (state: TSwapQuotes) => void +): TUnsubscribe => { + return subscribeEndpoint('swap/state', cb); +}; + +export type TProposeSwap = { + buyAsset: AccountCode; + sellAmount: string; + sellAsset: AccountCode; +}; + +export const proposeSwap = ( + data: TProposeSwap, +): Promise => { + return apiPost('swap/quote', data); +}; + +type TSwapFailed = FailResponse & { aborted: boolean }; +type TSwapExecutionResult = SuccessResponse | TSwapFailed; + +export const executeSwap = (): Promise => { + return apiPost('swap/execute'); +};