- 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.
625 lines
16 KiB
Go
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
|
|
}
|