Module: Sdp::Coverage

Defined in:
lib/sdp/coverage.rb

Overview

The curated SDP surface this gem covers, pinned against the vendored OpenAPI spec (spec/openapi-v0.31.json). Consumed by the contract tests (test/sdp/contract_test.rb) and the sdp:drift rake task.

Path templates use the param style of SDP’s generated spec. When a resource method is added or removed, this map changes in the same commit.

Defined Under Namespace

Classes: Endpoint

Constant Summary collapse

WALLET_FIELDS =

Fields read by Sdp::Wallet.from_hash.

%w[id walletId publicKey label status provider purpose createdAt balances].freeze
BALANCE_FIELDS =

Fields read by Sdp::Balance.from_hash. usdValue is optional upstream —presence in the schema’s properties is what we pin, not requiredness.

%w[token mint amount uiAmount decimals usdValue].freeze
TRANSFER_FIELDS =

Fields read by Sdp::Transfer.from_hash.

%w[id direction status signature token amount source destination memo error createdAt].freeze
TOKEN_FIELDS =

Fields read by Sdp::Token.from_hash. extensions is passed through untyped.

%w[id projectId signingWalletId mintAddress mintAuthority freezeAuthority name symbol
decimals description uri imageUrl template extensions totalSupply maxSupply isMintable
isFreezable requiresAllowlist status deployedAt createdAt updatedAt].freeze
TOKEN_TX_FIELDS =

Fields read by Sdp::TokenTransaction.from_hash (the mint/burn action record).

%w[id tokenId type status signature serializedTx params slot blockTime fee error
createdAt updatedAt].freeze
PREPARED_TX_FIELDS =

The unsigned-transaction envelope shared by the …/prepare responses.

%w[serialized blockhash lastValidBlockHeight].freeze
RAMP_QUOTE_FIELDS =

Fields read by Sdp::RampQuote.from_hash. The *Currency members are passed through untyped, so only their presence at data.quote is pinned.

%w[id provider status deliveryMode hostedUrl paymentInstructions exchangeRate
totalSendingAmount sendingCurrency totalReceivingAmount receivingCurrency
feesIncluded feeCurrency expiresAt].freeze
RAMP_EXECUTION_FIELDS =

Fields read by Sdp::RampExecution.from_hash.

%w[id provider status redirectUrl paymentInstructions reference].freeze
COVERED_ENDPOINTS =
[
  # NOTE: at v0.31 the initialize 201 response has NO data envelope —
  # configId/publicKey/walletId sit at the schema root.
  Endpoint.new(method: "post", path: "/v1/wallets/initialize", success_status: "201",
               reads: { [] => %w[configId publicKey walletId] }),
  Endpoint.new(method: "post", path: "/v1/wallets", success_status: "201",
               reads: { %w[data wallet] => WALLET_FIELDS }),
  Endpoint.new(method: "get", path: "/v1/wallets", success_status: "200",
               reads: { [ "data", "wallets", "[]" ] => WALLET_FIELDS }),
  Endpoint.new(method: "get", path: "/v1/payments/wallets/{walletId}/balances", success_status: "200",
               reads: { %w[data walletBalances] => %w[walletId balances],
                        [ "data", "walletBalances", "balances", "[]" ] => BALANCE_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/payments/transfers", success_status: "200",
               reads: { %w[data transfer] => TRANSFER_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/payments/transfers/prepare", success_status: "200",
               reads: { %w[data] => %w[transfer preparedTransaction simulation],
                        %w[data transfer] => TRANSFER_FIELDS,
                        %w[data preparedTransaction] => %w[serialized blockhash lastValidBlockHeight] }),
  Endpoint.new(method: "get", path: "/v1/payments/transfers", success_status: "200",
               reads: { [ "data", "[]" ] => TRANSFER_FIELDS }),
  Endpoint.new(method: "get", path: "/v1/payments/transfers/{transferId}", success_status: "200",
               reads: { %w[data transfer] => TRANSFER_FIELDS }),

  # Issuance — token lifecycle + supply actions (v0.2). list returns a bare
  # data array; create/get/deploy wrap the token in data.token. mint/burn
  # return the action record at data.transaction; mint also carries
  # data.tokenAccount. The prepare variants differ: deploy/prepare puts the
  # unsigned tx at data.transaction with a sibling data.mint, while
  # mint/burn prepare keep the record at data.transaction and the unsigned
  # tx at data.preparedTransaction.
  Endpoint.new(method: "get", path: "/v1/issuance/tokens", success_status: "200",
               reads: { [ "data", "[]" ] => TOKEN_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/issuance/tokens", success_status: "201",
               reads: { %w[data token] => TOKEN_FIELDS }),
  Endpoint.new(method: "get", path: "/v1/issuance/tokens/{tokenId}", success_status: "200",
               reads: { %w[data token] => TOKEN_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/issuance/tokens/{tokenId}/deploy", success_status: "200",
               reads: { %w[data token] => TOKEN_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/issuance/tokens/{tokenId}/deploy/prepare", success_status: "200",
               reads: { %w[data] => %w[transaction mint simulation],
                        %w[data transaction] => PREPARED_TX_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/issuance/tokens/{tokenId}/mint", success_status: "200",
               reads: { %w[data] => %w[transaction tokenAccount],
                        %w[data transaction] => TOKEN_TX_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/issuance/tokens/{tokenId}/mint/prepare", success_status: "200",
               reads: { %w[data] => %w[transaction preparedTransaction tokenAccount simulation],
                        %w[data transaction] => TOKEN_TX_FIELDS,
                        %w[data preparedTransaction] => PREPARED_TX_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/issuance/tokens/{tokenId}/burn", success_status: "200",
               reads: { %w[data transaction] => TOKEN_TX_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/issuance/tokens/{tokenId}/burn/prepare", success_status: "200",
               reads: { %w[data] => %w[transaction preparedTransaction simulation],
                        %w[data transaction] => TOKEN_TX_FIELDS,
                        %w[data preparedTransaction] => PREPARED_TX_FIELDS }),

  # Ramps (v0.2, sandbox-only). currency endpoints return nested discovery
  # data; quote wraps the record in data.quote, execute in data.ramp; the
  # sandbox hook returns data.transaction (untyped passthrough).
  Endpoint.new(method: "get", path: "/v1/payments/ramps/onramp/currency", success_status: "200",
               reads: { %w[data] => %w[currencies pairs supportHash],
                        %w[data currencies] => %w[sources destinations],
                        [ "data", "pairs", "[]" ] => %w[source dest providers] }),
  Endpoint.new(method: "get", path: "/v1/payments/ramps/offramp/currency", success_status: "200",
               reads: { %w[data] => %w[currencies pairs supportHash],
                        %w[data currencies] => %w[sources destinations],
                        [ "data", "pairs", "[]" ] => %w[source dest providers] }),
  Endpoint.new(method: "post", path: "/v1/payments/ramps/onramp/quote", success_status: "200",
               reads: { %w[data quote] => RAMP_QUOTE_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/payments/ramps/onramp/execute", success_status: "200",
               reads: { %w[data ramp] => RAMP_EXECUTION_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/payments/ramps/offramp/execute", success_status: "200",
               reads: { %w[data ramp] => RAMP_EXECUTION_FIELDS }),
  Endpoint.new(method: "post", path: "/v1/payments/ramps/sandbox/simulate", success_status: "200",
               reads: { %w[data] => %w[transaction] })
].freeze

Class Method Summary collapse

Class Method Details

.resolve(spec, node, depth = 0) ⇒ Object

Follows “$ref”: “#/components/schemas/X” pointers (recursively, with a depth guard). SDP’s generated spec is fully inlined today (zero $refs at v0.31) — this keeps the guard working if that changes.



141
142
143
144
145
146
147
148
149
# File 'lib/sdp/coverage.rb', line 141

def resolve(spec, node, depth = 0)
  return node unless node.is_a?(Hash) && node["$ref"].is_a?(String)
  return nil if depth > 10

  name = node["$ref"][%r{\A#/components/schemas/(.+)\z}, 1]
  return nil unless name

  resolve(spec, spec.dig("components", "schemas", name), depth + 1)
end

.success_schema(spec, endpoint) ⇒ Object

The application/json schema of the endpoint’s documented success response, $ref-resolved. nil when the spec doesn’t document it.



120
121
122
123
124
# File 'lib/sdp/coverage.rb', line 120

def success_schema(spec, endpoint)
  operation = spec.dig("paths", endpoint.path, endpoint.method)
  schema = operation&.dig("responses", endpoint.success_status, "content", "application/json", "schema")
  resolve(spec, schema)
end

.walk(spec, schema, segments) ⇒ Object

Navigates a (resolved) schema along a reads path. “[]” descends into array items; any other segment descends into that property. Returns the resolved node, or nil as soon as the path breaks.



129
130
131
132
133
134
135
136
# File 'lib/sdp/coverage.rb', line 129

def walk(spec, schema, segments)
  segments.reduce(resolve(spec, schema)) do |node, segment|
    break nil unless node

    child = segment == "[]" ? node["items"] : node.dig("properties", segment)
    resolve(spec, child)
  end
end