Skip to content

Commit 7c4b8d9

Browse files
committed
add backpack exchange, server endpoints
1 parent b711c7c commit 7c4b8d9

25 files changed

Lines changed: 1161 additions & 44 deletions

cmd/oc/start.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@ func NewStartCmd() *cobra.Command {
2020
if err != nil {
2121
return err
2222
}
23-
server := server.New(listen, config)
23+
serverConfig, err := server.LoadConfig(configPath)
24+
if err != nil {
25+
return err
26+
}
27+
server := server.New(listen, config, serverConfig)
2428
return server.Start()
2529
},
2630
}

defaults.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ offchain:
1616
#
1717

1818
exchanges:
19+
backpack:
20+
no_account_types: true
21+
1922
binance:
2023
# https://developers.binance.com/docs/sub_account/asset-management/Universal-Transfer
2124
account_types:

exchange.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ var (
1717
Binance ExchangeId = "binance"
1818
BinanceUS ExchangeId = "binanceus"
1919
Bybit ExchangeId = "bybit"
20+
Backpack ExchangeId = "backpack"
2021
)
2122

22-
var ValidExchangeIds = []ExchangeId{Okx, Binance, BinanceUS, Bybit}
23+
var ValidExchangeIds = []ExchangeId{Okx, Binance, BinanceUS, Bybit, Backpack}
2324

2425
type MultiSecret struct {
2526
ApiKeyRef secret.Secret `yaml:"api_key"`

exchanges/backpack/api/api.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package api
2+
3+
import (
4+
"crypto/ed25519"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"log/slog"
10+
"net/http"
11+
"net/url"
12+
"sort"
13+
"strings"
14+
"time"
15+
)
16+
17+
type Client struct {
18+
apiKey string // Base64 encoded public key
19+
privateKey ed25519.PrivateKey
20+
baseURL string
21+
httpClient *http.Client
22+
window int64 // Time window in milliseconds
23+
}
24+
25+
// NewClient creates a new Backpack API client
26+
func NewClient(apiKey string, privateKey ed25519.PrivateKey) (*Client, error) {
27+
return &Client{
28+
apiKey: apiKey,
29+
privateKey: privateKey,
30+
baseURL: "https://api.backpack.exchange",
31+
httpClient: &http.Client{},
32+
window: 5000, // Default window value
33+
}, nil
34+
}
35+
36+
func (c *Client) SetBaseURL(baseURL string) {
37+
c.baseURL = baseURL
38+
}
39+
40+
func (c *Client) SetWindow(window int64) {
41+
if window > 60000 {
42+
window = 60000 // Maximum allowed window
43+
}
44+
c.window = window
45+
}
46+
47+
// createSigningString creates the string to be signed for authentication
48+
func createSigningString(instruction string, params map[string]string, timestamp int64, window int64) string {
49+
// Sort keys alphabetically
50+
var keys []string
51+
for k := range params {
52+
keys = append(keys, k)
53+
}
54+
sort.Strings(keys)
55+
56+
// Build query string from sorted parameters
57+
var parts []string
58+
for _, k := range keys {
59+
parts = append(parts, fmt.Sprintf("%s=%s", k, params[k]))
60+
}
61+
62+
// Create the signing string with instruction, params, timestamp and window
63+
signingString := fmt.Sprintf("instruction=%s", instruction)
64+
if len(parts) > 0 {
65+
signingString += "&" + strings.Join(parts, "&")
66+
}
67+
signingString += fmt.Sprintf("&timestamp=%d&window=%d", timestamp, window)
68+
69+
return signingString
70+
}
71+
72+
// sign creates the signature for authentication
73+
func (c *Client) sign(instruction string, params map[string]string, timestamp int64) string {
74+
signingString := createSigningString(instruction, params, timestamp, c.window)
75+
fmt.Println("signingString", signingString)
76+
signature := ed25519.Sign(c.privateKey, []byte(signingString))
77+
return base64.StdEncoding.EncodeToString(signature)
78+
}
79+
80+
// Request makes an authenticated HTTP request to the Backpack API
81+
func (c *Client) Request(method, path, instruction string, input interface{}, output interface{}, query url.Values) ([]byte, error) {
82+
method = strings.ToUpper(method)
83+
apiUrl := c.baseURL + path
84+
85+
log := slog.With("method", method, "url", apiUrl, "instruction", instruction)
86+
87+
// Prepare parameters for signing
88+
params := make(map[string]string)
89+
90+
// Handle input body parameters
91+
var bodyStr string
92+
if input != nil {
93+
jsonBody, err := json.Marshal(input)
94+
if err != nil {
95+
return nil, fmt.Errorf("failed to marshal request body: %w", err)
96+
}
97+
bodyStr = string(jsonBody)
98+
99+
// Parse JSON body into map for signing
100+
var bodyMap map[string]interface{}
101+
if err := json.Unmarshal(jsonBody, &bodyMap); err != nil {
102+
return nil, fmt.Errorf("failed to unmarshal request body for signing: %w", err)
103+
}
104+
105+
// Convert all values to strings for the signing map
106+
for k, v := range bodyMap {
107+
params[k] = fmt.Sprintf("%v", v)
108+
}
109+
}
110+
111+
// Handle query parameters
112+
for k, values := range query {
113+
if len(values) > 0 {
114+
params[k] = values[0]
115+
}
116+
}
117+
118+
log.Debug("request", "body", bodyStr, "params", params)
119+
120+
// Generate timestamp
121+
timestamp := time.Now().UnixMilli()
122+
123+
// Generate signature
124+
signature := c.sign(instruction, params, timestamp)
125+
126+
// Create request
127+
var reqBody io.Reader
128+
if bodyStr != "" {
129+
reqBody = strings.NewReader(bodyStr)
130+
}
131+
132+
// Append query to URL if needed
133+
if len(query) > 0 {
134+
apiUrl += "?" + query.Encode()
135+
}
136+
137+
req, err := http.NewRequest(method, apiUrl, reqBody)
138+
if err != nil {
139+
return nil, fmt.Errorf("failed to create request: %w", err)
140+
}
141+
142+
// Set headers
143+
req.Header.Set("Content-Type", "application/json")
144+
req.Header.Set("X-API-Key", c.apiKey)
145+
req.Header.Set("X-Timestamp", fmt.Sprintf("%d", timestamp))
146+
req.Header.Set("X-Window", fmt.Sprintf("%d", c.window))
147+
req.Header.Set("X-Signature", signature)
148+
149+
resp, err := c.httpClient.Do(req)
150+
if err != nil {
151+
return nil, fmt.Errorf("failed to send request: %w", err)
152+
}
153+
defer resp.Body.Close()
154+
155+
respBody, err := io.ReadAll(resp.Body)
156+
if err != nil {
157+
return nil, fmt.Errorf("failed to read response body: %w", err)
158+
}
159+
log.Debug("response", "status", resp.StatusCode, "body", string(respBody))
160+
161+
if resp.StatusCode != http.StatusOK {
162+
var backpackError struct {
163+
Code int `json:"code"`
164+
Message string `json:"message"`
165+
}
166+
if err := json.Unmarshal(respBody, &backpackError); err == nil {
167+
return nil, fmt.Errorf("request failed with code %d: %s", backpackError.Code, backpackError.Message)
168+
}
169+
return nil, fmt.Errorf("request failed %d: %s", resp.StatusCode, string(respBody))
170+
}
171+
172+
if output != nil {
173+
err = json.Unmarshal(respBody, output)
174+
if err != nil {
175+
return nil, fmt.Errorf("failed to unmarshal response body: %w", err)
176+
}
177+
}
178+
179+
return respBody, nil
180+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package api
2+
3+
import (
4+
oc "github.com/cordialsys/offchain"
5+
)
6+
7+
type Token struct {
8+
Blockchain oc.NetworkId `json:"blockchain"`
9+
ContractAddress oc.ContractAddress `json:"contractAddress"`
10+
DepositEnabled bool `json:"depositEnabled"`
11+
WithdrawEnabled bool `json:"withdrawEnabled"`
12+
13+
MinimumDeposit oc.Amount `json:"minimumDeposit"`
14+
15+
MinimumWithdraw oc.Amount `json:"minimumWithdraw"`
16+
MaximumWithdrawal oc.Amount `json:"maximumWithdrawal"`
17+
18+
WithdrawalFee oc.Amount `json:"withdrawalFee"`
19+
}
20+
21+
type Asset struct {
22+
Symbol string `json:"symbol"`
23+
Tokens []*Token `json:"tokens"`
24+
}
25+
26+
// https://docs.backpack.exchange/#tag/Assets/operation/get_assets
27+
func (c *Client) GetAssets() ([]Asset, error) {
28+
var response []Asset
29+
30+
// No instruction type is specified in the docs, using a generic "assetQuery"
31+
// This might need to be updated based on actual API requirements
32+
_, err := c.Request("GET", "/api/v1/assets", "assetQuery", nil, &response, nil)
33+
if err != nil {
34+
return nil, err
35+
}
36+
37+
return response, nil
38+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package api
2+
3+
import (
4+
oc "github.com/cordialsys/offchain"
5+
)
6+
7+
// Balance represents the balance information for a specific asset
8+
type Balance struct {
9+
Available oc.Amount `json:"available"`
10+
Locked oc.Amount `json:"locked"`
11+
Staked oc.Amount `json:"staked"`
12+
}
13+
14+
type BalanceResponse map[string]Balance
15+
16+
// https://docs.backpack.exchange/#tag/Capital/operation/get_balances
17+
func (c *Client) GetBalances() (BalanceResponse, error) {
18+
var response BalanceResponse
19+
20+
// No query parameters or request body needed for this endpoint
21+
_, err := c.Request("GET", "/api/v1/capital", "balanceQuery", nil, &response, nil)
22+
if err != nil {
23+
return nil, err
24+
}
25+
26+
return response, nil
27+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/url"
6+
7+
oc "github.com/cordialsys/offchain"
8+
)
9+
10+
type DepositAddressRequest struct {
11+
Blockchain oc.NetworkId `json:"blockchain"`
12+
}
13+
14+
// DepositAddressResponse represents the response from the deposit address endpoint
15+
type DepositAddressResponse struct {
16+
Address string `json:"address"`
17+
}
18+
19+
func (c *Client) GetDepositAddress(req *DepositAddressRequest) (*DepositAddressResponse, error) {
20+
if req == nil || req.Blockchain == "" {
21+
return nil, fmt.Errorf("blockchain is required")
22+
}
23+
24+
query := url.Values{}
25+
query.Set("blockchain", string(req.Blockchain))
26+
27+
var response DepositAddressResponse
28+
_, err := c.Request("GET", "/wapi/v1/capital/deposit/address", "depositAddressQuery", nil, &response, query)
29+
if err != nil {
30+
return nil, err
31+
}
32+
33+
return &response, nil
34+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package api
2+
3+
type SubaccountMapping struct {
4+
Id int `json:"id"`
5+
Name string `json:"name"`
6+
}
7+
8+
type SubaccountResponse []SubaccountMapping
9+
10+
// No documentation for this endpoint, had to snoop it on the browser
11+
func (c *Client) GetSubaccounts() (SubaccountResponse, error) {
12+
var response SubaccountResponse
13+
_, err := c.Request("GET", "/wapi/v1/subaccount", "subaccountQueryAll", nil, &response, nil)
14+
if err != nil {
15+
return nil, err
16+
}
17+
return response, nil
18+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package api
2+
3+
import (
4+
"net/url"
5+
"strconv"
6+
)
7+
8+
type WithdrawalHistoryRequest struct {
9+
From *int64 `json:"from,omitempty"`
10+
To *int64 `json:"to,omitempty"`
11+
Limit *uint64 `json:"limit,omitempty"`
12+
Offset *uint64 `json:"offset,omitempty"`
13+
}
14+
15+
// https://docs.backpack.exchange/#tag/Capital/operation/get_withdrawals
16+
func (c *Client) GetWithdrawals(req *WithdrawalHistoryRequest) ([]WithdrawalResponse, error) {
17+
query := url.Values{}
18+
19+
if req != nil {
20+
if req.From != nil {
21+
query.Set("from", strconv.FormatInt(*req.From, 10))
22+
}
23+
if req.To != nil {
24+
query.Set("to", strconv.FormatInt(*req.To, 10))
25+
}
26+
if req.Limit != nil {
27+
query.Set("limit", strconv.FormatUint(*req.Limit, 10))
28+
}
29+
if req.Offset != nil {
30+
query.Set("offset", strconv.FormatUint(*req.Offset, 10))
31+
}
32+
}
33+
34+
var response []WithdrawalResponse
35+
_, err := c.Request("GET", "/wapi/v1/capital/withdrawals", "withdrawalQueryAll", nil, &response, query)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
return response, nil
41+
}

0 commit comments

Comments
 (0)