synor/sdk/go/bridge/client.go
Gulshan Yadav 74b82d2bb2 Add Synor Storage and Wallet SDKs for Swift
- Implement SynorStorage class for decentralized storage operations including upload, download, pinning, and CAR file management.
- Create supporting types and models for storage operations such as UploadOptions, Pin, and StorageConfig.
- Implement SynorWallet class for wallet operations including wallet creation, address generation, transaction signing, and balance queries.
- Create supporting types and models for wallet operations such as Wallet, Address, and Transaction.
- Introduce error handling for both storage and wallet operations.
2026-01-27 01:56:45 +05:30

625 lines
16 KiB
Go

// Package bridge provides cross-chain asset transfer functionality.
package bridge
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"sync/atomic"
"time"
)
// Client is a Synor Bridge client for cross-chain transfers.
type Client struct {
config Config
httpClient *http.Client
closed atomic.Bool
}
// NewClient creates a new bridge client with the given configuration.
func NewClient(config Config) *Client {
if config.Endpoint == "" {
config.Endpoint = DefaultEndpoint
}
if config.Timeout == 0 {
config.Timeout = 60 * time.Second
}
if config.Retries == 0 {
config.Retries = 3
}
return &Client{
config: config,
httpClient: &http.Client{
Timeout: config.Timeout,
},
}
}
// ==================== Chain Operations ====================
// GetSupportedChains returns all supported chains.
func (c *Client) GetSupportedChains(ctx context.Context) ([]Chain, error) {
var resp struct {
Chains []Chain `json:"chains"`
}
if err := c.request(ctx, "GET", "/chains", nil, &resp); err != nil {
return nil, err
}
return resp.Chains, nil
}
// GetChain returns chain information by ID.
func (c *Client) GetChain(ctx context.Context, chainID ChainID) (*Chain, error) {
var chain Chain
if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s", chainID), nil, &chain); err != nil {
return nil, err
}
return &chain, nil
}
// IsChainSupported checks if a chain is supported.
func (c *Client) IsChainSupported(ctx context.Context, chainID ChainID) bool {
chain, err := c.GetChain(ctx, chainID)
if err != nil {
return false
}
return chain.Supported
}
// ==================== Asset Operations ====================
// GetSupportedAssets returns supported assets for a chain.
func (c *Client) GetSupportedAssets(ctx context.Context, chainID ChainID) ([]Asset, error) {
var resp struct {
Assets []Asset `json:"assets"`
}
if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s/assets", chainID), nil, &resp); err != nil {
return nil, err
}
return resp.Assets, nil
}
// GetAsset returns asset information by ID.
func (c *Client) GetAsset(ctx context.Context, assetID string) (*Asset, error) {
var asset Asset
if err := c.request(ctx, "GET", fmt.Sprintf("/assets/%s", assetID), nil, &asset); err != nil {
return nil, err
}
return &asset, nil
}
// GetWrappedAsset returns the wrapped asset mapping.
func (c *Client) GetWrappedAsset(ctx context.Context, originalAssetID string, targetChain ChainID) (*WrappedAsset, error) {
var wrapped WrappedAsset
path := fmt.Sprintf("/assets/%s/wrapped/%s", originalAssetID, targetChain)
if err := c.request(ctx, "GET", path, nil, &wrapped); err != nil {
return nil, err
}
return &wrapped, nil
}
// GetWrappedAssets returns all wrapped assets for a chain.
func (c *Client) GetWrappedAssets(ctx context.Context, chainID ChainID) ([]WrappedAsset, error) {
var resp struct {
Assets []WrappedAsset `json:"assets"`
}
if err := c.request(ctx, "GET", fmt.Sprintf("/chains/%s/wrapped", chainID), nil, &resp); err != nil {
return nil, err
}
return resp.Assets, nil
}
// ==================== Fee & Rate Operations ====================
// EstimateFee estimates the bridge fee for a transfer.
func (c *Client) EstimateFee(ctx context.Context, asset, amount string, sourceChain, targetChain ChainID) (*FeeEstimate, error) {
body := map[string]interface{}{
"asset": asset,
"amount": amount,
"sourceChain": sourceChain,
"targetChain": targetChain,
}
var estimate FeeEstimate
if err := c.request(ctx, "POST", "/fees/estimate", body, &estimate); err != nil {
return nil, err
}
return &estimate, nil
}
// GetExchangeRate returns the exchange rate between two assets.
func (c *Client) GetExchangeRate(ctx context.Context, fromAsset, toAsset string) (*ExchangeRate, error) {
var rate ExchangeRate
path := fmt.Sprintf("/rates/%s/%s", url.PathEscape(fromAsset), url.PathEscape(toAsset))
if err := c.request(ctx, "GET", path, nil, &rate); err != nil {
return nil, err
}
return &rate, nil
}
// ==================== Lock-Mint Flow ====================
// Lock locks assets on the source chain for cross-chain transfer.
// This is step 1 of the lock-mint flow.
func (c *Client) Lock(ctx context.Context, asset, amount string, targetChain ChainID, opts *LockOptions) (*LockReceipt, error) {
body := map[string]interface{}{
"asset": asset,
"amount": amount,
"targetChain": targetChain,
}
if opts != nil {
if opts.Recipient != "" {
body["recipient"] = opts.Recipient
}
if opts.Deadline != 0 {
body["deadline"] = opts.Deadline
}
if opts.Slippage != 0 {
body["slippage"] = opts.Slippage
}
}
var receipt LockReceipt
if err := c.request(ctx, "POST", "/transfers/lock", body, &receipt); err != nil {
return nil, err
}
return &receipt, nil
}
// GetLockProof gets the lock proof for minting.
// This is step 2 of the lock-mint flow.
func (c *Client) GetLockProof(ctx context.Context, lockReceiptID string) (*LockProof, error) {
var proof LockProof
path := fmt.Sprintf("/transfers/lock/%s/proof", lockReceiptID)
if err := c.request(ctx, "GET", path, nil, &proof); err != nil {
return nil, err
}
return &proof, nil
}
// WaitForLockProof waits for the lock proof to be ready.
func (c *Client) WaitForLockProof(ctx context.Context, lockReceiptID string, pollInterval, maxWait time.Duration) (*LockProof, error) {
if pollInterval == 0 {
pollInterval = 5 * time.Second
}
if maxWait == 0 {
maxWait = 10 * time.Minute
}
deadline := time.Now().Add(maxWait)
for time.Now().Before(deadline) {
proof, err := c.GetLockProof(ctx, lockReceiptID)
if err == nil {
return proof, nil
}
if bridgeErr, ok := err.(*Error); ok && bridgeErr.Code == "CONFIRMATIONS_PENDING" {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(pollInterval):
continue
}
}
return nil, err
}
return nil, &Error{Message: "Timeout waiting for lock proof", Code: "CONFIRMATIONS_PENDING"}
}
// Mint mints wrapped tokens on the target chain.
// This is step 3 of the lock-mint flow.
func (c *Client) Mint(ctx context.Context, proof *LockProof, targetAddress string, opts *MintOptions) (*SignedTransaction, error) {
body := map[string]interface{}{
"proof": proof,
"targetAddress": targetAddress,
}
if opts != nil {
if opts.GasLimit != "" {
body["gasLimit"] = opts.GasLimit
}
if opts.MaxFeePerGas != "" {
body["maxFeePerGas"] = opts.MaxFeePerGas
}
if opts.MaxPriorityFeePerGas != "" {
body["maxPriorityFeePerGas"] = opts.MaxPriorityFeePerGas
}
}
var tx SignedTransaction
if err := c.request(ctx, "POST", "/transfers/mint", body, &tx); err != nil {
return nil, err
}
return &tx, nil
}
// ==================== Burn-Unlock Flow ====================
// Burn burns wrapped tokens on the current chain.
// This is step 1 of the burn-unlock flow.
func (c *Client) Burn(ctx context.Context, wrappedAsset, amount string, opts *BurnOptions) (*BurnReceipt, error) {
body := map[string]interface{}{
"wrappedAsset": wrappedAsset,
"amount": amount,
}
if opts != nil {
if opts.Recipient != "" {
body["recipient"] = opts.Recipient
}
if opts.Deadline != 0 {
body["deadline"] = opts.Deadline
}
}
var receipt BurnReceipt
if err := c.request(ctx, "POST", "/transfers/burn", body, &receipt); err != nil {
return nil, err
}
return &receipt, nil
}
// GetBurnProof gets the burn proof for unlocking.
// This is step 2 of the burn-unlock flow.
func (c *Client) GetBurnProof(ctx context.Context, burnReceiptID string) (*BurnProof, error) {
var proof BurnProof
path := fmt.Sprintf("/transfers/burn/%s/proof", burnReceiptID)
if err := c.request(ctx, "GET", path, nil, &proof); err != nil {
return nil, err
}
return &proof, nil
}
// WaitForBurnProof waits for the burn proof to be ready.
func (c *Client) WaitForBurnProof(ctx context.Context, burnReceiptID string, pollInterval, maxWait time.Duration) (*BurnProof, error) {
if pollInterval == 0 {
pollInterval = 5 * time.Second
}
if maxWait == 0 {
maxWait = 10 * time.Minute
}
deadline := time.Now().Add(maxWait)
for time.Now().Before(deadline) {
proof, err := c.GetBurnProof(ctx, burnReceiptID)
if err == nil {
return proof, nil
}
if bridgeErr, ok := err.(*Error); ok && bridgeErr.Code == "CONFIRMATIONS_PENDING" {
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(pollInterval):
continue
}
}
return nil, err
}
return nil, &Error{Message: "Timeout waiting for burn proof", Code: "CONFIRMATIONS_PENDING"}
}
// Unlock unlocks original tokens on the source chain.
// This is step 3 of the burn-unlock flow.
func (c *Client) Unlock(ctx context.Context, proof *BurnProof, opts *UnlockOptions) (*SignedTransaction, error) {
body := map[string]interface{}{
"proof": proof,
}
if opts != nil {
if opts.GasLimit != "" {
body["gasLimit"] = opts.GasLimit
}
if opts.GasPrice != "" {
body["gasPrice"] = opts.GasPrice
}
}
var tx SignedTransaction
if err := c.request(ctx, "POST", "/transfers/unlock", body, &tx); err != nil {
return nil, err
}
return &tx, nil
}
// ==================== Transfer Management ====================
// GetTransfer returns a transfer by ID.
func (c *Client) GetTransfer(ctx context.Context, transferID string) (*Transfer, error) {
var transfer Transfer
if err := c.request(ctx, "GET", fmt.Sprintf("/transfers/%s", transferID), nil, &transfer); err != nil {
return nil, err
}
return &transfer, nil
}
// GetTransferStatus returns the status of a transfer.
func (c *Client) GetTransferStatus(ctx context.Context, transferID string) (TransferStatus, error) {
transfer, err := c.GetTransfer(ctx, transferID)
if err != nil {
return "", err
}
return transfer.Status, nil
}
// ListTransfers returns transfers matching the filter.
func (c *Client) ListTransfers(ctx context.Context, filter *TransferFilter) ([]Transfer, error) {
params := url.Values{}
if filter != nil {
if filter.Status != "" {
params.Set("status", string(filter.Status))
}
if filter.SourceChain != "" {
params.Set("sourceChain", string(filter.SourceChain))
}
if filter.TargetChain != "" {
params.Set("targetChain", string(filter.TargetChain))
}
if filter.Asset != "" {
params.Set("asset", filter.Asset)
}
if filter.Sender != "" {
params.Set("sender", filter.Sender)
}
if filter.Recipient != "" {
params.Set("recipient", filter.Recipient)
}
if filter.FromDate != nil {
params.Set("fromDate", strconv.FormatInt(filter.FromDate.Unix(), 10))
}
if filter.ToDate != nil {
params.Set("toDate", strconv.FormatInt(filter.ToDate.Unix(), 10))
}
if filter.Limit > 0 {
params.Set("limit", strconv.Itoa(filter.Limit))
}
if filter.Offset > 0 {
params.Set("offset", strconv.Itoa(filter.Offset))
}
}
path := "/transfers"
if len(params) > 0 {
path = "/transfers?" + params.Encode()
}
var resp struct {
Transfers []Transfer `json:"transfers"`
}
if err := c.request(ctx, "GET", path, nil, &resp); err != nil {
return nil, err
}
return resp.Transfers, nil
}
// WaitForTransfer waits for a transfer to complete.
func (c *Client) WaitForTransfer(ctx context.Context, transferID string, pollInterval, maxWait time.Duration) (*Transfer, error) {
if pollInterval == 0 {
pollInterval = 10 * time.Second
}
if maxWait == 0 {
maxWait = 30 * time.Minute
}
finalStatuses := map[TransferStatus]bool{
TransferStatusCompleted: true,
TransferStatusFailed: true,
TransferStatusRefunded: true,
}
deadline := time.Now().Add(maxWait)
for time.Now().Before(deadline) {
transfer, err := c.GetTransfer(ctx, transferID)
if err != nil {
return nil, err
}
if finalStatuses[transfer.Status] {
return transfer, nil
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(pollInterval):
continue
}
}
return nil, &Error{Message: "Timeout waiting for transfer completion"}
}
// ==================== Convenience Methods ====================
// BridgeTo executes a complete lock-mint transfer.
func (c *Client) BridgeTo(ctx context.Context, asset, amount string, targetChain ChainID, targetAddress string, lockOpts *LockOptions, mintOpts *MintOptions) (*Transfer, error) {
// Lock on source chain
lockReceipt, err := c.Lock(ctx, asset, amount, targetChain, lockOpts)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Locked: %s, waiting for confirmations...\n", lockReceipt.ID)
}
// Wait for proof
proof, err := c.WaitForLockProof(ctx, lockReceipt.ID, 0, 0)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Proof ready, minting on %s...\n", targetChain)
}
// Mint on target chain
_, err = c.Mint(ctx, proof, targetAddress, mintOpts)
if err != nil {
return nil, err
}
// Return final transfer status
return c.WaitForTransfer(ctx, lockReceipt.ID, 0, 0)
}
// BridgeBack executes a complete burn-unlock transfer.
func (c *Client) BridgeBack(ctx context.Context, wrappedAsset, amount string, burnOpts *BurnOptions, unlockOpts *UnlockOptions) (*Transfer, error) {
// Burn wrapped tokens
burnReceipt, err := c.Burn(ctx, wrappedAsset, amount, burnOpts)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Burned: %s, waiting for confirmations...\n", burnReceipt.ID)
}
// Wait for proof
proof, err := c.WaitForBurnProof(ctx, burnReceipt.ID, 0, 0)
if err != nil {
return nil, err
}
if c.config.Debug {
fmt.Printf("Proof ready, unlocking on %s...\n", burnReceipt.TargetChain)
}
// Unlock on original chain
_, err = c.Unlock(ctx, proof, unlockOpts)
if err != nil {
return nil, err
}
// Return final transfer status
return c.WaitForTransfer(ctx, burnReceipt.ID, 0, 0)
}
// ==================== Lifecycle ====================
// Close closes the client.
func (c *Client) Close() {
c.closed.Store(true)
}
// IsClosed returns whether the client is closed.
func (c *Client) IsClosed() bool {
return c.closed.Load()
}
// HealthCheck performs a health check.
func (c *Client) HealthCheck(ctx context.Context) bool {
var resp struct {
Status string `json:"status"`
}
if err := c.request(ctx, "GET", "/health", nil, &resp); err != nil {
return false
}
return resp.Status == "healthy"
}
// ==================== Private Methods ====================
func (c *Client) request(ctx context.Context, method, path string, body interface{}, result interface{}) error {
if c.IsClosed() {
return &Error{Message: "Client has been closed"}
}
var lastErr error
for attempt := 0; attempt < c.config.Retries; attempt++ {
err := c.doRequest(ctx, method, path, body, result)
if err == nil {
return nil
}
if c.config.Debug {
fmt.Printf("Attempt %d failed: %v\n", attempt+1, err)
}
lastErr = err
if attempt < c.config.Retries-1 {
time.Sleep(time.Duration(1<<attempt) * time.Second)
}
}
return lastErr
}
func (c *Client) doRequest(ctx context.Context, method, path string, body interface{}, result interface{}) error {
url := c.config.Endpoint + path
var bodyReader io.Reader
if body != nil {
jsonBody, err := json.Marshal(body)
if err != nil {
return err
}
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequestWithContext(ctx, method, url, bodyReader)
if err != nil {
return err
}
req.Header.Set("Authorization", "Bearer "+c.config.APIKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-SDK-Version", "go/0.1.0")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode >= 400 {
var errorResp struct {
Message string `json:"message"`
Error string `json:"error"`
Code string `json:"code"`
}
json.Unmarshal(respBody, &errorResp)
message := errorResp.Message
if message == "" {
message = errorResp.Error
}
if message == "" {
message = fmt.Sprintf("HTTP %d", resp.StatusCode)
}
return &Error{
Message: message,
Code: errorResp.Code,
StatusCode: resp.StatusCode,
}
}
if result != nil {
if err := json.Unmarshal(respBody, result); err != nil {
return err
}
}
return nil
}
// Error represents a bridge error.
type Error struct {
Message string
Code string
StatusCode int
}
func (e *Error) Error() string {
return e.Message
}