What one provider pays for staking one asset.
Rates carry their unit, because venues do not agree on one
Gemini publishes rate in basis points, ratePct as a percentage, and apyPct as an
annualised percentage — three numbers for the same position, differing by a factor of a
hundred and by compounding. A contract that carried "the rate" would be inviting a caller
to be wrong by 100×, and the error would look plausible either way.
So this carries percentages only, both named for what they are:
:rate_pct— the simple rate, as a percentage:apy_pct— the annualised yield, as a percentage
A venue publishing basis points converts on the way in. A venue publishing only one of the
two leaves the other nil rather than deriving it: turning a simple rate into an APY needs
a compounding frequency the venue did not state, and assuming one is inventing a number.
:deposit_limit_usd is a real constraint
Venues cap how much new notional can earn the advertised rate. A caller staking past the cap does not get an error — it gets a worse rate on the excess, silently. Carried where the venue publishes it.
Summary
Types
Functions
Converts a rate in basis points to a percentage, or nil for a value it cannot read.
Provided here rather than left to each venue package, because it is the conversion most likely to be done inconsistently — and a rate wrong by 100× still looks like a rate.
What it refuses, and why it used to accept it
A caller passes this whatever the venue sent, which is the entire reason it exists. It
used to hand a binary straight to Decimal.new/1, and that is wrong in both directions:
It raised.
Decimal.new("")andDecimal.new("n/a")raiseDecimal.Error, so a venue omitting a rate, or naming it in words, crashed the caller rather than reporting an absent rate.Decimal.new(" 500 ")raises too — padding is not a number this function should die on.It accepted
"NaN"and"Infinity". Both parse, so both became aDecimaland then a rate. Every venue package in this family already refuses them, with the measurement recorded beside each copy:Decimal.add(nan, 1)is NaN and poisons a consumer's arithmetic silently,Decimal.compare(nan, _)raises in the consumer's own process naming Decimal rather than the venue, and an Infinity is quieter still — it compares greater than everything and never raises at all.
So this now answers nil for anything it cannot read, which is exactly the convention
every venue's own decimal/1 follows. The shared helper was less careful than the five
copies it exists to replace.
nil is a rate the venue did not state. It is not zero, which is a rate.
Builds a t/0, failing closed if a required field is absent or nil.
@enforce_keys guards presence, not nil — see DpExchange.Core.Types.Validate.