offchain

package module
v0.0.0-...-8eea3e4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 8, 2025 License: Apache-2.0 Imports: 17 Imported by: 0

README

Offchain

Offchain is a server that consolidates API keys in a central, secure location, and presents a universal API that can be used to manage funds. This is intended for making withdrawals, sub-account transfers, and deposits. Trading is out of scope.

Being able to withdraw funds directly from an exchange is a very sensitive operation. If you withdraw to the wrong address, those funds could be lost forever. This necessitates a separate application to take 'custody' of withdrawal-permissioned API keys.

Features
  • Simple configuration
  • Single binary
  • Rich exchange support and incredibly lightweight framework to easily add more
  • Strong authentication using ed25519 based http-signatures.
  • Stateless
  • Universal asset symbology (based on Cordial Systems asset registry)
  • Can store exchange API keys in popular secret managers (vault, gcp, aws, etc).
  • Ability exchange exchange operations on CLI.
  • Stable OpenAPI API

Install

go install github.com/cordialsys/offchain

Usage

First, configure API keys for supported exchanges.

# config.yaml
offchain:
  # setup API keys for the exchanges
  exchanges:
    binance:
      # load from your favorite secret manager
      api_key: "gcp:your_gcp_project,API_KEY_NAME"
      secret_key: "gcp:your_gcp_project,API_SECRET_NAME"
      # include any sub-accounts
      subaccounts:
        - id: "[email protected]"
          api_key: "gcp:your_gcp_project,SUB1_API_KEY_NAME"
          secret_key: "gcp:your_gcp_project,SUB1_API_KEY_SECRET_NAME"
        # ...

API keys can be loaded from env, file, or from you favorite secret manager (see oc secret --help).

Second, generate a ed25519 key to authenticate to the offchain server. HTTP Signatures are used.

You can generate a client-side key like so (private key will be written to disk).

oc keys generate mykey
# client-keys/mykey
# e7a205bbe21184f4f6cd72e7ba659566d96b8aea00e49b9967328b0109f9c706

Now update your server configuration again with the public key.

offchain:
  server:
    public_keys:
      - id: "mykey"
        key: "abf9649d7a0a7534cde49f12de47effd601e60a2258e51b5a257af9ef78e901f"

Start the server.

oc start --config ./config.yaml -v

Now make requests against it.

# Lookup main account balances
oc api balances --exchange binance --sign-with mykey

# Lookup balance of a sub-account
oc api balances --subaccount [email protected] --exchange binance --sign-with mykey

# Make sub-account transfer from main to sub-account
oc api --exchange binance transfer --to [email protected]  --symbol USDC --amount 3 --sign-with mykey
# From sub-account to main
oc api --exchange binance transfer --from [email protected]  --symbol USDC --amount 2 --sign-with mykey

# Make a withdrawal from main account
oc api --exchange binance withdraw --to "<your-solana-address>" --network SOL --symbol USDC --sign-with mykey

# Look at withdrawal history
oc api --exchange binance history --sign-with mykey

Policy

To further enhance the security, policies should be built on top of offchain. For example, you should check that a withdrawal address is approved before signing a request to offchain.

Cordial Treasury integrates with offchain and includes rich Transfer policies. It is default deny, and allows you to easily build allow-lists, notional transfer limits, and approval/quorom conditions.

Cordial Treasury will enforce policies, then sign requests to offchain using an MPC key. Both Cordial Treasury and offchain are non-custodial, self-hosted products.

Supported Exchanges

Exchange clients are lightweight, pure go implementations. It's very easy to add support for new exchanges.

  • Backpack
  • Binance
  • Binance US
  • Bybit
  • Okx

API Reference

See the API reference.

For http-authorization, you can look at or use the simple http-signature package.

Crosschain

We maintain a similar library, crosschain. It's like offchain, but solely for blockchains. You can locally generate addresses and make transfers on many different blockchains (as well as manage staking).

You could use both oc and xc to fully rebalance and/or take custody of your portfolio across all different exchanges and blockchain networks.

Roadmap

Universal asset symbols

We are still building out the universal asset integration. If you have interest in this, reach out to us at https://cordialsystems.com/contact and we can escalate.

Documentation

Index

Constants

View Source
const ENV_OFFCHAIN_CONFIG = "OFFCHAIN_CONFIG"
View Source
const FLOAT_PRECISION = 6

Variables

View Source
var ValidExchangeIds = []ExchangeId{Okx, Binance, BinanceUS, Bybit, Backpack}

Functions

func ApplyDefaults

func ApplyDefaults(cfg *ExchangeConfig)

Types

type Account

type Account struct {
	Id         AccountId
	Alias      string
	SubAccount bool
	MultiSecret
}

func (*Account) IsMain

func (cfg *Account) IsMain() bool

func (*Account) LoadSecrets

func (cfg *Account) LoadSecrets() error

type AccountId

type AccountId string

type AccountType

type AccountType string

type AccountTypeConfig

type AccountTypeConfig struct {
	// The ID for the account type used by the exchange (e.g. "SPOT" or "ISOLATED_MARGIN" on binance).
	Type AccountType `yaml:"type" json:"type"`
	// Human readable description of the account type.
	Description string `yaml:"description,omitempty" json:"description,omitempty"`
	// Any aliases for the account type
	Aliases []string `yaml:"aliases,omitempty" json:"aliases,omitempty"`
}

type Address

type Address string

type Amount

type Amount decimal.Decimal

Amount is a decimal amount as a human expects it for readability.

func NewAmountFromString

func NewAmountFromString(str string) (Amount, error)

NewAmountFromString creates a new AmountHumanReadable from a string

func NewAmountHumanReadableFromFloat

func NewAmountHumanReadableFromFloat(float float64) Amount

NewAmountHumanReadableFromFloat creates a new AmountHumanReadable from a float

func (Amount) Decimal

func (amount Amount) Decimal() decimal.Decimal

func (Amount) Div

func (amount Amount) Div(x Amount) Amount

func (Amount) IsZero

func (b Amount) IsZero() bool

func (Amount) MarshalJSON

func (b Amount) MarshalJSON() ([]byte, error)

func (Amount) MarshalYAML

func (b Amount) MarshalYAML() (interface{}, error)

func (Amount) String

func (amount Amount) String() string

func (Amount) ToBlockchain

func (amount Amount) ToBlockchain(decimals int32) Balance

func (*Amount) UnmarshalJSON

func (b *Amount) UnmarshalJSON(p []byte) error

func (*Amount) UnmarshalYAML

func (b *Amount) UnmarshalYAML(node *yaml.Node) error

type Asset

type Asset struct {
	SymbolId        SymbolId        `json:"symbol_id"`
	NetworkId       NetworkId       `json:"network_id"`
	ContractAddress ContractAddress `json:"contract_address"`
}

func NewAsset

func NewAsset(symbolId SymbolId, networkId NetworkId, contractAddress ContractAddress) *Asset

type Balance

type Balance big.Int

Balance is a big integer amount as blockchain expects it for tx.

func MultiplyByFloat

func MultiplyByFloat(amount Balance, multiplier float64) Balance

func NewAmountBlockchainFromInt64

func NewAmountBlockchainFromInt64(i64 int64) (Balance, bool)

NewAmountBlockchainFromUint64 creates a new AmountBlockchain from a uint64

func NewAmountBlockchainFromStr

func NewAmountBlockchainFromStr(str string) Balance

NewAmountBlockchainFromStr creates a new AmountBlockchain from a string

func NewAmountBlockchainFromUint64

func NewAmountBlockchainFromUint64(u64 uint64) Balance

NewAmountBlockchainFromUint64 creates a new AmountBlockchain from a uint64

func NewAmountBlockchainToMaskFloat64

func NewAmountBlockchainToMaskFloat64(f64 float64) Balance

NewAmountBlockchainToMaskFloat64 creates a new AmountBlockchain as a float64 times 10^FLOAT_PRECISION

func (*Balance) Abs

func (amount *Balance) Abs() Balance

func (*Balance) Add

func (amount *Balance) Add(x *Balance) Balance

Use the underlying big.Int.Add()

func (Balance) Bytes

func (amount Balance) Bytes() []byte

func (*Balance) Cmp

func (amount *Balance) Cmp(other *Balance) int

Use the underlying big.Int.Cmp()

func (*Balance) Div

func (amount *Balance) Div(x *Balance) Balance

Use the underlying big.Int.Div()

func (Balance) Int

func (amount Balance) Int() *big.Int

Int converts an AmountBlockchain into *bit.Int

func (*Balance) IsZero

func (amount *Balance) IsZero() bool

func (Balance) MarshalJSON

func (b Balance) MarshalJSON() ([]byte, error)

func (*Balance) Mul

func (amount *Balance) Mul(x *Balance) Balance

Use the underlying big.Int.Mul()

func (Balance) Sign

func (amount Balance) Sign() int

func (Balance) String

func (amount Balance) String() string

func (*Balance) Sub

func (amount *Balance) Sub(x *Balance) Balance

Use the underlying big.Int.Sub()

func (*Balance) ToHuman

func (amount *Balance) ToHuman(decimals int32) Amount

func (Balance) Uint64

func (amount Balance) Uint64() uint64

Uint64 converts an AmountBlockchain into uint64

func (*Balance) UnmarshalJSON

func (b *Balance) UnmarshalJSON(p []byte) error

func (Balance) UnmaskFloat64

func (amount Balance) UnmaskFloat64() float64

UnmaskFloat64 converts an AmountBlockchain into float64 given the number of decimals

type Config

type Config struct {
	Exchanges map[ExchangeId]*ExchangeConfig `yaml:"exchanges"`
}

func LoadUnvalidatedConfig

func LoadUnvalidatedConfig(configPathMaybe string) (*Config, error)

func (*Config) GetExchange

func (c *Config) GetExchange(id ExchangeId) (*ExchangeConfig, bool)

func (*Config) Init

func (cfg *Config) Init() error

type ContractAddress

type ContractAddress string

type ExchangeClientConfig

type ExchangeClientConfig struct {
	ApiUrl string `yaml:"api_url"`

	// The account types supported by the exchange.
	AccountTypes   []*AccountTypeConfig `yaml:"account_types"`
	NoAccountTypes *bool                `yaml:"no_account_types,omitempty"`
}

func GetDefaultConfig

func GetDefaultConfig(exchangeId ExchangeId) (ExchangeClientConfig, bool)

type ExchangeConfig

type ExchangeConfig struct {
	ExchangeId           ExchangeId `yaml:"exchange"`
	ExchangeClientConfig `yaml:",inline"`

	// ApiKeyRef     secret.Secret `yaml:"api_key"`
	// SecretKeyRef  secret.Secret `yaml:"secret_key"`
	// PassphraseRef secret.Secret `yaml:"passphrase"`
	MultiSecret `yaml:",inline"`

	// Id of the main account, if required by the exchange.
	Id AccountId `yaml:"id"`

	// Subaccounts are isolated accounts on an exchange.  They have their own API keys and are
	// typically used for trading.
	SubAccounts []*SubAccount `yaml:"subaccounts"`
}

Main configuration for an exchange

func (*ExchangeConfig) AsAccount

func (cfg *ExchangeConfig) AsAccount() *Account

func (*ExchangeConfig) FirstAccountType

func (cfg *ExchangeConfig) FirstAccountType() (accountCfg *AccountTypeConfig, ok bool)

func (*ExchangeConfig) ResolveAccountType

func (cfg *ExchangeConfig) ResolveAccountType(typeOrAlias string) (accountCfg *AccountTypeConfig, ok bool, message string)

func (*ExchangeConfig) ResolveSubAccount

func (cfg *ExchangeConfig) ResolveSubAccount(idOrAlias string) (accountCfg *SubAccount, ok bool)

type ExchangeId

type ExchangeId string
var (
	Okx       ExchangeId = "okx"
	Binance   ExchangeId = "binance"
	BinanceUS ExchangeId = "binanceus"
	Bybit     ExchangeId = "bybit"
	Backpack  ExchangeId = "backpack"
)

type MultiSecret

type MultiSecret struct {
	ApiKeyRef     secret.Secret `yaml:"api_key"`
	SecretKeyRef  secret.Secret `yaml:"secret_key"`
	PassphraseRef secret.Secret `yaml:"passphrase"`

	// Loads all api_key, secret_key, passphrase if this is set.
	// The format must either be:
	// - separated by newlines in order of (<api_key>, <secret_key>, [passphrase])
	// - or a JSON object with the keys "api_key", "secret_key", and optionally "passphrase"
	SecretsRef secret.Secret `yaml:"secrets"`
}

func (*MultiSecret) LoadSecrets

func (c *MultiSecret) LoadSecrets() error

type NetworkId

type NetworkId string

type StringOrInt

type StringOrInt string

func (StringOrInt) AsInt

func (s StringOrInt) AsInt() (uint64, bool)

func (StringOrInt) AsString

func (s StringOrInt) AsString() string

type SubAccount

type SubAccount struct {
	SubAccountHeader `yaml:",inline"`
	MultiSecret      `yaml:",inline"`
}

A subaccount is an isolated account on an exchange. It's just like a main account, but typically you cannot withdraw funds from it directly. Instead, funds must be transferred to the main account first. Subaccounts have their own independent API keys.

func (*SubAccount) AsAccount

func (cfg *SubAccount) AsAccount() *Account

type SubAccountHeader

type SubAccountHeader struct {
	// The id of the subaccount is required.  This is created on the exchange by the user.
	// If it does not match, then it won't work.
	Id    AccountId `yaml:"id" json:"id"`
	Alias string    `yaml:"alias,omitempty" json:"alias,omitempty"`
}

type SymbolId

type SymbolId string

Directories

Path Synopsis
cmd
oc command
exchanges
okx
pkg
hex
client/api
Package api provides primitives to interact with the openapi HTTP API.
Package api provides primitives to interact with the openapi HTTP API.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL