Class: Sdp::Client

Inherits:
Object
  • Object
show all
Includes:
Resources::Payments, Resources::Wallets
Defined in:
lib/sdp/client.rb

Overview

Zero-dependency Net::HTTP request core for the SDP API.

Success envelope: { “data”: …, “meta”: … } Error envelope: { “error”: { “code”, “message”, “details” }, “meta”: … }

Retry policy: GETs retry once on Timeout/Unavailable. POSTs NEVER retry —SDP has no idempotency key, so re-sending a transfer risks a double-spend.

Endpoint methods live in resource modules layered on top of the #get/#post primitives below; this class owns auth, envelope handling, the typed error mapping, and the retry posture.

Defined Under Namespace

Classes: Response

Constant Summary collapse

DEFAULT_BASE_URL =
"http://127.0.0.1:8787"
OPEN_TIMEOUT =

seconds — fail fast when the stack isn’t up

2
READ_TIMEOUT =

seconds — transfer confirmation is synchronous

10
NOT_FOUND_HINT =

Wallet-scoped API keys return 404 (not 403) for wallets outside their scope, which reads like “does not exist” when it really means “not yours”. Appended to every NotFound so the failure is diagnosable.

"(hint: wallet-scoped API keys return 404 for wallets outside their scope)"

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from Resources::Payments

#create_transfer, #get_transfer, #list_transfers, #prepare_transfer

Methods included from Resources::Wallets

#create_wallet, #initialize_custody, #list_wallets, #wallet_balances

Constructor Details

#initialize(base_url: ENV.fetch("SDP_API_BASE_URL", DEFAULT_BASE_URL), api_key: ENV["SDP_API_KEY"], open_timeout: OPEN_TIMEOUT, read_timeout: READ_TIMEOUT) ⇒ Client

Returns a new instance of Client.



45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/sdp/client.rb', line 45

def initialize(base_url: ENV.fetch("SDP_API_BASE_URL", DEFAULT_BASE_URL),
               api_key: ENV["SDP_API_KEY"],
               open_timeout: OPEN_TIMEOUT,
               read_timeout: READ_TIMEOUT)
  # Strip first, then guard: an ENV key with a trailing newline passes a
  # naive blank-check but then makes every request raise a raw ArgumentError
  # from the "Bearer …\n" header. Normalize once, at the boundary.
  @api_key = api_key.to_s.strip
  if @api_key.empty?
    raise ConfigurationError, "SDP_API_KEY is missing or blank. " \
      "Pass api_key: or set the SDP_API_KEY environment variable."
  end

  @base_url = base_url.to_s.chomp("/")
  # base_url is documented as ConfigurationError-covered, so validate it at
  # boot (mirroring the api_key fail-fast) instead of letting an unusable
  # URL surface as a cryptic transport error on the first request.
  parsed = begin
    URI.parse(@base_url)
  rescue URI::InvalidURIError
    nil
  end
  unless parsed.is_a?(URI::HTTP) && !parsed.host.to_s.empty?
    raise ConfigurationError, "SDP_API_BASE_URL is invalid: expected an http(s) URL with a " \
      "host, got #{@base_url.inspect}. Pass base_url: or set the SDP_API_BASE_URL environment variable."
  end

  @open_timeout = open_timeout
  @read_timeout = read_timeout
end

Instance Attribute Details

#base_urlObject (readonly)

Returns the value of attribute base_url.



43
44
45
# File 'lib/sdp/client.rb', line 43

def base_url
  @base_url
end

Instance Method Details

#get(path, query: nil) ⇒ Object

Reads are safe to retry exactly once on transport-level failures.



88
89
90
91
92
93
# File 'lib/sdp/client.rb', line 88

def get(path, query: nil)
  uri = build_uri(path, query)
  with_read_retry do
    perform(Net::HTTP::Get.new(uri), uri, idempotent: true)
  end
end

#inspectObject

Redacted on purpose: the API key is a bearer secret and must never leak into consoles, logs, or exception-capture payloads via the default #inspect (which would dump every instance variable, @api_key included).



79
80
81
# File 'lib/sdp/client.rb', line 79

def inspect
  "#<#{self.class} base_url=#{@base_url.inspect}>"
end

#post(path, payload = nil) ⇒ Object



95
96
97
98
99
100
101
# File 'lib/sdp/client.rb', line 95

def post(path, payload = nil)
  uri = build_uri(path, nil)
  request = Net::HTTP::Post.new(uri)
  request["Content-Type"] = "application/json"
  request.body = JSON.generate(payload) unless payload.nil?
  perform(request, uri, idempotent: false) # no retry wrapper — writes are never retried
end