Skip to content

@openrampkit/adapter ​

The adapter API: createAdapter(), the Adapter interface, helpers for provider calls, and a test kit at @openrampkit/adapter/testing. For a walkthrough, read Writing an adapter.

createAdapter(definition) ​

ts
import { createAdapter, ADAPTER_API_VERSION } from '@openrampkit/adapter'

const adapter = createAdapter({ id: 'acme', name: 'Acme Pay', legs, quote, start })

Throws when id is not lowercase letters, digits or dashes, or when two legs share an id. Sets apiVersion to ADAPTER_API_VERSION (2). A definition with another apiVersion throws, with a message that tells you what to update. See Upgrade from version 1.

Adapter ​

ts
interface Adapter {
  id: string
  name: string
  apiVersion: number
  readonly env?: 'sandbox' | 'production'   // AdapterEnv. Undefined: follows each session's livemode
  legs: LegSpec[]
  catalog?(input: CatalogInput, ctx: Pick<AdapterContext, 'fetch' | 'log' | 'shared'>): Promise<LegSpec[]>
  sourceTokens?(input: { chains: string[]; method: string }, ctx: Pick<AdapterContext, 'fetch' | 'log' | 'shared'>): Promise<SourceToken[]>
  standingAddress?(input: StandingAddressInput, ctx: Pick<AdapterContext, 'fetch' | 'log' | 'shared'>):
    Promise<{ address: string; ref: string; data?: Record<string, unknown> }>
  standingAccount?(input: StandingAccountInput, ctx: Pick<AdapterContext, 'fetch' | 'log' | 'shared'>): Promise<StandingAccountResult>
  standingDeposits?(input: StandingDepositsInput | StandingAccountDepositsInput, ctx: Pick<AdapterContext, 'fetch' | 'log' | 'shared'>): Promise<StandingDeposit[]>
  quote(input: QuoteInput, ctx: AdapterContext): Promise<LegQuote>
  prepareDeposit?(input: { leg: PathwayLeg; amountIn?: Amount }, ctx: AdapterContext):
    Promise<{ address: string; ref?: string; data?: Record<string, unknown> }>
  start(input: StartInput, ctx: AdapterContext): Promise<LegStep>
  transition?(input: TransitionInput, ctx: AdapterContext): Promise<LegStep>
  status?(input: { leg: PathwayLeg; ref: string }, ctx: AdapterContext): Promise<LegStep>
  webhook?: {
    // Optional: false when the options have no webhook secret, so no event can verify. Default true.
    configured?: boolean
    verify(req: Request, rawBody: string, ctx: WebhookContext): Promise<boolean>
    parse(rawBody: string, ctx: WebhookContext & { url?: string }): Promise<LegEvent[]>
    // Optional: the same key for every delivery of one provider event. The server ignores a repeat for 7 days.
    replayKey?(req: Request, rawBody: string, ctx: WebhookContext): Promise<string | undefined>
  }
  health?(ctx: Pick<AdapterContext, 'fetch' | 'log'>): Promise<{ ok: boolean; detail?: string }>
  routes?(req: Request, subpath: string, ctx: RouteContext): Promise<Response | undefined>
}
MemberCalled by the server
envAt start: checked against livemode (a sandbox adapter stops a live server; a production adapter in a test server gets a warning)
legsAt plan time, when there is no catalog or it fails
catalogAt plan time, with { country?, currency, direction }
quotePOST /quotes, once per leg of each quoted pathway
prepareDepositBefore quoting a pathway, for every leg after the first
startPOST /select for the first leg; after a leg succeeds, for the next one
transitionPOST /transitions/:name for SUBMIT and SURFACE_RESULT transitions
statusGET /step (at most every 2 s per leg), sweep() and sessions.refresh()
webhookPOST /webhooks/:adapterId
healthGET /health?deep=1 with the tasks token (plain GET /health does not call adapters)
sourceTokensGET /sessions/:id/sources, with { chains, method }. Return the tokens the user can pay with, most popular first. The server checks and caches them.
routesAny request to /adapters/:adapterId/*
standingAddressdepositAddresses.get(), the first time for a user and route. Return the same address for the same input, and keep it in shared with no TTL or a very long one.
standingAccountdepositAddresses.get() with a fiat origin (origin: { currency }), the first time for a user and route. Return a standing bank account: its main identifier, a ref, and the bank details.
standingDepositssweep(), for each standing address that is due, with the ref from standingAddress (or standingAccount) and since (ms). Return every deposit seen since then. The server makes one session per new id.

An adapter with standing addresses needs both methods, status() (the server polls each deposit with its legRef), and a leg with the method transfer (the deposit sessions use it). checkAdapterShape checks this. See Standing deposit addresses.

standingRoutes (optional) says which routes the addresses serve. ['convert'] is the default: the origin is another token or chain, and the provider converts each deposit. ['same']: the origin is the destination token on the destination chain. Then standingAddress must return the destination address itself (the server refuses another address with 502), and the adapter reports the transfers it sees on chain. ['bank']: the origin is a fiat currency paid by bank transfer. The adapter then needs standingAccount, and standingDeposits gets a StandingAccountDepositsInput. The server picks, for each route, the first adapter that serves it. The chain-watch adapter serves same. The Bridge adapter serves convert and bank with the option standing: true.

standingCurrencies (optional, with standingAccount): the ISO 4217 currencies of the standing bank accounts. The widget offers a bank account only for these (server option depositAddresses.widget).

ts
type StandingAddressInput = {
  userId: string
  origin: { chain: string; token: string }             // the asset the user sends
  destination: Extract<Destination, { type: 'crypto' }> // where each deposit goes
}

type StandingRoute = 'convert' | 'same' | 'bank' // Adapter.standingRoutes?: StandingRoute[]

type StandingDepositsInput = { ref: string; address: string; origin: { chain: string; token: string }; since: number }

// A standing bank account (route `bank`)
type StandingAccountInput = { userId: string; origin: { currency: string }; destination: Extract<Destination, { type: 'crypto' }> }
type StandingAccountResult = {
  address: string // the main account identifier (account number, IBAN, CLABE or Pix code)
  ref: string
  bank: { fields: Array<{ label: string; value: string; copy: boolean }> }
  data?: Record<string, unknown>
}
type StandingAccountDepositsInput = { ref: string; address: string; origin: { currency: string }; since: number }

type StandingDeposit = {
  id: string      // the same for one deposit on every call: a provider request id, or the deposit tx hash
  legRef: string  // a ref that status() understands for this one deposit
  at: number      // when the provider saw the deposit (ms)
  input?: Amount  // the deposit amount, when known (for a bank account: in the origin currency)
}

resultChannels(adapter) returns { polling, webhooks }: polling is true when the adapter has status(), and webhooks is true when it has a webhook whose configured is not false. The server uses it at start. It writes one warning for each adapter with legs that has neither, because the payments of that adapter cannot complete. Leg capabilities do not say how results arrive: LegSpec.capabilities has only settlement and surface_after_processing.

Inputs ​

ts
type CatalogInput = { country?: string; currency: string; direction: Direction }

type QuoteInput = {
  leg: PathwayLeg
  amountIn?: Amount        // exactly one side is set
  amountOut?: Amount
  deliverTo?: { address: string }
  source?: { chain: string; token: string; address?: string }
}

type StartInput = {
  leg: PathwayLeg
  quote: LegQuote
  deliverTo?: { address: string }
  source?: { chain: string; token: string; address?: string }
  /** The client asks to pay without native gas (gas abstraction). An adapter may return WALLET_SIGN; others ignore it. */
  gasless?: boolean
}

type TransitionInput = { leg: PathwayLeg; ref: string; name: string; inputs?: Record<string, unknown> }

type LegEvent = LegStep & {
  ref: string
  eventId?: string           // provider event id: the server drops an id that the session already applied
}

A LegEvent is a LegStep with the leg's ref (see Legs and the session step). The server applies it with the same rules as a step from status(). Examples:

  • A payment: { ref, status: 'succeeded', output, transactions: [{ role: 'destination', hash }] }.
  • A KYC review: { ref, status: 'processing', phase: 'kyc' }. The leg stays in KYC.
  • An offramp that learns its deposit address: { ref, status: 'requires_action', action: { kind: 'payment', surface: { kind: 'WALLET_TX', ... }, transitions } }.

Context ​

ts
interface AdapterContext {
  session: {
    id: string; userId: string; direction: Direction; locale: string; livemode: boolean
    country?: string; region?: string; email?: string; ip?: string
  }
  destination: Destination
  pathway: { legs: PathwayLeg[]; index: number }
  urls: { returnUrl: string; webhookUrl: string }
  store: ScopedKV      // this adapter, this session
  shared: ScopedKV     // this adapter, all sessions
  fetch: typeof fetch
  log: Logger
  idempotencyKey(scope: string): string   // `${sessionId}:${scope}`
}

interface ScopedKV {
  get<T = unknown>(key: string): Promise<T | undefined>
  put(key: string, value: unknown, ttlSec?: number): Promise<void>
  /** Optional: write only when the key has no live value, as one atomic step. True when it wrote. */
  putIfAbsent?(key: string, value: unknown, ttlSec: number): Promise<boolean>
}

type WebhookContext = Pick<AdapterContext, 'log' | 'shared' | 'fetch'>

type RouteContext = Pick<AdapterContext, 'fetch' | 'log' | 'shared'> & {
  baseUrl: string
  applyEvent(event: LegEvent): Promise<void>   // same effect as a webhook
}

putIfAbsent is set when the server's store has an atomic operation for it: memoryStore, redisStore and durableObjectStore have it, cloudflareKvStore does not. Do not call it directly. Use claimOnce.

ctx.session.locale is en when the app did not set one. ctx.session.ip is the end user's IP from the latest browser request, when known.

One owner per record ​

ts
claimOnce(shared: ScopedKV, key: string, owner: string, ttlSec: number): Promise<boolean>

Records key as used by owner, for example a transaction hash, a log, or a provider deposit that can complete one payment only. Returns true when owner holds the key after the call. It also returns true when owner held the key before, so a retry gets the same answer. Returns false when another owner holds the key. The stored value is the owner string, with the TTL ttlSec.

When shared.putIfAbsent is set, the claim is atomic: when two claims run at the same time, exactly one wins. When it is not set, claimOnce reads the key, writes it when it is free, then reads it back. This catches most races, but not all.

ts
const key = `txused:${chain}:${txHash.toLowerCase()}`
if (!(await claimOnce(ctx.shared, key, `${ctx.session.id}:${ref}`, 90 * 24 * 3600))) {
  return fail('This transaction was already used for another payment.')
}

Webhook replay keys ​

ts
webhookBodyKey(rawBody: string): Promise<string>       // SHA-256 of the body, hex
claimWebhook(shared, key, ttlSec = WEBHOOK_REPLAY_TTL_SEC): Promise<string | undefined>
releaseWebhook(shared, key, token, ttlSec = WEBHOOK_REPLAY_TTL_SEC): Promise<void>

The server uses these with webhook.replayKey. claimWebhook returns a token for the first delivery of a key, and undefined for a repeat within WEBHOOK_REPLAY_TTL_SEC (7 days). It is built on claimOnce. releaseWebhook gives the key back (only with the token), so the next delivery can take it. The server releases a key when it cannot apply the events yet and answers 503. An adapter only returns the key from replayKey: it does not call these functions.

HTTP helpers ​

ExportDescription
fetchJson<T>(fetch, url, init?)JSON request with accept: application/json, content-type when there is a body, and a timeout (init.timeoutMs, default DEFAULT_TIMEOUT_MS = 8000). Throws an HttpError with status and parsed body on non-2xx, timeout: true on timeout, and a clear error for non-JSON bodies.
httpErrorToOpenRamp(e, provider, { what?, noQuoteStatuses?, log?, setupHint? })OpenRampException passes through; 429 gives RATE_LIMITED (429); noQuoteStatuses (default 400, 404, 409, 422) give NO_QUOTES (422) with the provider's message; 401 and 403 give a setup error (see below); a timeout gives PROVIDER_UNAVAILABLE (504); anything else gives PROVIDER_UNAVAILABLE (502) and a warning log
findDeliverAsset(list, asset)The entry of list ({ chain, token, symbol?, decimals? }) that delivers asset: same chain and token (EVM addresses without case). undefined on no match. It never falls back to another entry.
requireDeliverAsset(list, asset, provider)findDeliverAsset, or NO_QUOTES (422, recovery choose_other) "{provider} does not deliver {token} on {chain}." Use it in quote(), so the user never gets a quote for another token.
deliverableToAsset(d)The CryptoAsset of a list entry, with symbol and decimals when known
providerSetupError(provider)The setup error: PROVIDER_UNAVAILABLE (502), retryable: false, recovery choose_other, message "{provider} is not set up for this app yet. Try another method." Use it in an adapter with its own error mapping.
httpStatus(e)The numeric status of an error, or undefined
providerMessage(e)The provider's message from body.message, body.errorMessage or body.error(.message)

Types: HttpError, FetchJsonInit, HttpErrorOptions.

A 401 or 403 from a provider means that the provider refused our credentials or setup (API key, environment, IP allowlist). A retry cannot fix it. httpErrorToOpenRamp returns providerSetupError(provider), so the user sees a neutral message and can choose another method. It also writes one error log for the operator. The log names the provider and the HTTP status, and tells the operator what to check. Give setupHint to add a provider-specific fix to the log, for example "Set relay({ apiKey })".

Step helpers ​

ExportDescription
POLLonchain (2.5 s start, 10 s max, 30 min), checkout (4 s, 15 s, 60 min), dev (1.5 s, 5 s, 15 min)
awaitPoll(poll, name = 'poll')An AWAIT transition
awaitingPayment(ref, poll)A requires_action step while the user pays in a provider page: { status: 'requires_action', action: { kind: 'payment', transitions: [awaitPoll(poll)] }, ref }. It has no surface, so the UI keeps the current one.
legStepFromEvent(event, ref, poll)The status() answer for a provider order that you mapped to a LegEvent (the same mapping as the webhook parser). No event: awaitingPayment(ref, poll). Else the event without eventId, with poll on a step that waits, and with an AWAIT poll action on a requires_action event without an action. It sets no state: the server uses stateFor().
decimalFrom(n, digits = 8)Provider number to an exact decimal string; missing or non-finite gives '0'
minWithToleranceBps(expectedBase, bps)The smallest amount (integer base units, as a string) that still counts as expectedBase when it can be up to bps basis points lower: expected - floor(expected * bps / 10000), with bigint math. minWithToleranceBps('999', 50) is '995'.
randomHex(bytes = 8)Random hex string
bytesToHex(bytes)Lowercase hex of a Uint8Array or ArrayBuffer, no 0x
base64ToBytes(b64), bytesToBase64(bytes)Standard base64 (not base64url). base64ToBytes ignores whitespace and throws on other characters.
hmacSha256(secret, message, 'hex' | 'base64')WebCrypto HMAC
timingSafeEqual(a, b)Constant-time string compare (the same function as in @openrampkit/core)

Shared adapter helpers ​

ExportDescription
quoteExpiresAt(minutes = 5, providerExpiry?)The expiresAt of a quote (ISO 8601). Every LegQuote must have one. It uses the provider's expiry (an ISO string or milliseconds) when it is a valid time in the future and not later than minutes from now. Else minutes from now. DEFAULT_QUOTE_TTL_MINUTES is 5.
statusMap(provider, table, { ignoreCase? })A typed table from provider statuses to your values (for example a LegStatus and a detail code). Use it in place of a switch with a default branch. An unknown status gives undefined and one warning log per value. It never falls back to another entry: you decide what an unknown status means (usually: keep the current step). .known lists the statuses.
verifyTimestampedHmac({ secret, rawBody, header, timestamp?, timestampKey?, signatureKey?, toleranceSec?, message?, encoding? })Checks an HMAC-SHA256 webhook signature over a timestamp and the body, with a time window (default 300 s). Defaults: header t=...,v1=..., message {t}.{body}, hex. False for a missing secret, header or timestamp, a timestamp outside the window, or a wrong signature. Constant-time compare. Stripe, MoonPay, Coinbase, Peer and Meld use it.
parseSignatureHeader(header)Parses key=value pairs separated by commas (t=1700000000,v1=...). A key can repeat, so each key maps to a list.
cachedJson(kv, key, ttlSec, load, { valid? })Reads key from kv, or runs load and keeps its result for ttlSec seconds. A value that valid refuses (for example an empty list) is not kept. Use it for provider catalogs, rates and token data.
ts
import { statusMap, quoteExpiresAt } from '@openrampkit/adapter'

const STATUS = statusMap('Acme', {
  waitingPayment: { status: 'requires_action' },
  pending: { status: 'processing', detail: 'settling' },
  completed: { status: 'succeeded' },
  failed: { status: 'failed' },
})
const m = STATUS(order.status, ctx.log) // undefined for an unknown status
const expiresAt = quoteExpiresAt(5, order.quoteExpiresAt)

RSA signatures ​

For providers that sign webhooks with RSA (RSASSA-PKCS1-v1_5 with SHA-256, also known as SHA256withRSA), for example Binance and Bridge.

ExportDescription
rsaVerify(publicKey, data, signatureB64)Checks a base64 signature of data (a string is UTF-8 encoded; a Uint8Array is used as given). WebCrypto hashes data with SHA-256 first. publicKey is an SPKI key as PEM or base64, or a CryptoKey. Returns false for a wrong or malformed signature. Throws when the key string is not a valid public key.
importRsaPublicKey(key)Imports an SPKI public key (PEM or base64) once, for many rsaVerify calls
rsaKeyDer(key, 'public' | 'private')The DER bytes of a PEM or base64 key. Accepts \n escapes from environment variables. Throws for a PKCS#1 PEM (RSA PUBLIC KEY, RSA PRIVATE KEY): convert it with openssl rsa -pubout or openssl pkcs8 -topk8.
ts
import { importRsaPublicKey, rsaVerify } from '@openrampkit/adapter'

let key: Promise<CryptoKey> | undefined
async function verify(rawBody: string, signature: string) {
  key ??= importRsaPublicKey(opts.webhookPublicKey)
  return rsaVerify(await key, rawBody, signature)
}

EVM helpers ​

Plain JSON-RPC over fetch. No viem.

ExportDescription
evmRpc(fetch, url, method, params, { log? })One JSON-RPC call. A network, HTTP or RPC error becomes PROVIDER_UNAVAILABLE (502, or 504 on a timeout).
erc20TransferData(to, amountBase)ERC-20 transfer(to, amount) calldata
erc20PaidTo(receipt, token, recipient)The sum of ERC-20 Transfer logs of token to recipient in a receipt, in base units (bigint)
ERC20_TRANSFER_TOPIC, topicAddress(address)The Transfer event topic, and an address as a 32-byte topic, for eth_getLogs filters
EvmReceiptThe receipt fields the helpers read (status, blockNumber, logs)
hashTypedData(data)The EIP-712 digest of a TypedDataRequest (nested structs, arrays, bytes, string, integers and fixed bytes)
recoverAddress(digest, signature)The lower-case address that signed a 32-byte digest, from a 65-byte r || s || v signature (v 27/28 or 0/1). undefined when the signature is not valid.
recoverTypedDataSigner(data, signature)The signer of EIP-712 typed data, or undefined. Use it to check a WALLET_SIGN signature before you send it to a provider.

Solana helpers ​

ExportDescription
solanaPaidTo(tx, owner, mint)The amount (base units, bigint) that the transfer instructions of a parsed transaction (getTransaction with jsonParsed) send to owner. With mint: 'native', it counts System Program transfers in lamports. With an SPL mint, it counts SPL Token and Token-2022 transfer and transferChecked instructions into a token account of owner. A failed transaction gives 0n.

| solanaNetReceived(tx, owner, mint) | The net amount (base units, bigint) that a transaction moved to owner, from the balance changes: lamports for mint: 'native', else the sum over the token accounts of mint that owner owns. Negative when the owner sent more than it got. | | solanaMintDecimals(tx, mint) | The decimals of mint from the token balances of the transaction (9 for native), or undefined |

Types: SolanaParsedTx, SolanaParsedInstruction, SolanaTokenBalance, SolanaSignatureStatus, SolanaBalanceTx.

Chain watch ​

createChainWatch(options) watches chains with no provider: JSON-RPC calls with per-chain URLs and a rate limit, ERC-20 Transfer logs page by page, and Solana reads. The Relay adapter (same-chain transfers) and the chain-watch adapter use it. Make one per adapter instance: the rate limit is per instance.

OptionTypeDefaultDescription
rpcUrlsRecord<string, string>noneJSON-RPC URL per CAIP-2 chain. It wins over defaultRpcUrls.
defaultRpcUrlsRecord<string, string>PUBLIC_RPC_URLSThe URLs of the chains that rpcUrls does not name. Pass {} to use only rpcUrls. The public RPCs have rate limits: set your own in production.
logBlockRangenumber2000The most blocks in one eth_getLogs call. At most 10000.
maxLogPagesnumber5The most eth_getLogs pages in one check. At most 50. The next check goes on from where this one stopped.
requestsPerSecondnumberno limitThe most RPC requests per second to one RPC URL, in this process. Calls wait for their turn.
timeoutMsnumber8000The timeout of one RPC call
confirmationsnumber | Record<string, number>12EVM: the blocks on top of a log's block before the log counts. One number, or one per chain (other chains get 12).
solanaCommitment'confirmed' | 'finalized''finalized'The commitment of Solana reads
MethodDescription
rpc(ctx, chain, method, params)One JSON-RPC call. A chain with no URL throws PROVIDER_UNAVAILABLE ("No RPC is configured to verify transfers on ...").
rpcUrl(chain)The URL for the chain, or undefined
latestBlock(ctx, chain)eth_blockNumber as a bigint
confirmationsFor(chain)The confirmation depth of the chain
transferLogPages(ctx, { chain, token, to, from, latest })An async generator of pages { from, to, logs }: the Transfer logs of token to to, at most logBlockRange blocks and maxLogPages pages. Each page has its logs oldest first, with removed logs left out.
solanaSignatures(ctx, chain, account, { limit, before?, until?, commitment? })getSignaturesForAddress, newest first
solanaTokenAccounts(ctx, chain, owner, mint)The token accounts of mint that owner owns
solanaTx(ctx, chain, signature, commitment?)getTransaction with jsonParsed, or null

Also exported: txUsedKey(chain, hash) (the claimOnce key of a used transaction; add :{logIndex} for one log; the server keeps every txused: key in one namespace for all adapters, so a transfer that one adapter claimed cannot complete a payment of another), claimTransferLog(shared, chain, hash, logIndex, owner, ttlSec) (claim one Transfer log: first its transaction for logs, with the value TX_LOGS_OWNER, then the log; a wallet payment that claims the same transaction key with its own ref then loses, or wins first, so exactly one of them counts the transfer), TX_LOGS_OWNER, PUBLIC_RPC_URLS, DEFAULT_LOG_BLOCK_RANGE, MAX_LOG_BLOCK_RANGE, DEFAULT_LOG_PAGES, MAX_LOG_PAGES, DEFAULT_CONFIRMATIONS.

Settlement helpers ​

Helpers for the OpenRampSettlement contract. See On-chain settlement and the settlement flow.

ExportDescription
buildSettlementTxs({ chainId, contract, sessionId, token, amount, recipient, calls?, intent? })The two WALLET_TX transactions: approve, then settle
encodeSettle(params, intent?, { fromBalance? })settle (or settleFromBalance) calldata. With fromBalance, the intent minAmount must equal amount.
erc20ApproveData(spender, amountBase)ERC-20 approve calldata
settlementIntentTypedData({ chainId, contract, sessionId, token, recipient, minAmount, calls?, deadline, payer? })The EIP-712 typed data for the intent signer, for settle
settlementBalanceIntentTypedData({ chainId, contract, sessionId, token, recipient, amount, calls?, deadline, payer? })The EIP-712 typed data for settleFromBalance. It binds the exact amount.
hashSettlementCalls(calls)The calls hash, equal to the contract hashCalls and to callsHash in Settled
settlementCallsFrom(destination.calls)ContractCall[] to SettlementCall[]. Throws on a bad address, bad data or native value.
verifySettlement({ rpcUrl, contract, sessionId, fromBlock?, expect? })Reads receiptOf and the Settled log. Returns { settled: false } or { settled: true, ok, problem?, record }.
sessionIdToBytes32(id), bytes32ToSessionId(word)The session id as bytes32 (UTF-8, padded with zeros), and back
isEvmAddress(value), keccak256(data)Small utilities
OPEN_RAMP_SETTLEMENT_ABI, SETTLEMENT_SELECTORS, SETTLED_TOPIC, SETTLEMENT_INTENT_TYPES, SETTLEMENT_BALANCE_INTENT_TYPESThe ABI, the function selectors, the Settled topic and the EIP-712 types

Types: SettlementCall, SettlementIntent, SettlementParams, SettlementIntentTypedData, SettlementBalanceIntentTypedData, SettlementRecord, VerifySettlementResult.

Conformance checks (main entry) ​

ExportDescription
checkAdapterShape(adapter)API version 2, legs, ETAs, surfaces, region policy, decimal limits, known capabilities. Declared capabilities and surfaces need their methods: surface_after_processing needs a webhook; settlement needs a crypto to asset at an address; a FORM, OTP or WALLET_TX surface needs transition(). An adapter with legs needs a result channel (status() or a configured webhook).
checkLegQuote(quote)Decimal strings; an ISO expiresAt in the future; a known guarantee; minOutput with min_output, in the asset of output; slippageBps an integer from 0 to 10000; each fee with a boolean included and an amount that is null or an Amount with a valid asset
checkLegStep(step)The v2 step rules: a known status; an action (known kind, transitions) with requires_action and with no other status; phase only with pending or processing; detail.code in STEP_DETAIL_CODES; transaction roles (not hop) and hashes; legal transitions. It flags the v1 fields state, sub, txHash, sourceTxHash, surface and transitions.
LEG_STATUSESEvery LegStatus
sameQuotedAsset(a, b)Same fiat currency, or same chain and token. A wildcard (*) matches anything.

Each returns ConformanceProblem[] ({ where, problem }).

@openrampkit/adapter/testing ​

Test helpers. They do not import a test runner.

ts
import {
  runAdapterConformance, fakeFetch, makeCtx, makeWebhookCtx, memoryKV, recordingLog, silentLog, TEST_DESTINATION,
} from '@openrampkit/adapter/testing'
ExportDescription
fakeFetch(routes){ fetch, calls }. Each route: method?, match (substring or RegExp), reply?(call) (JSON value, a Response, or throw), status? (default 200), hang?. First match wins; unmatched calls get 404. calls records { method, url, headers, body?, raw? }.
makeCtx({ fetch, destination?, pathway?, shared?, store?, session?, urls?, log? })An AdapterContext: session sess_1, user user_1, US, en, test mode; USDC on Base to 0x...beef; fresh memoryKV() stores
makeWebhookCtx({ fetch?, shared?, log? })A WebhookContext
memoryKV()A ScopedKV with TTLs checked against Date.now() (fake timers work). Has data.
recordingLog()A logger that records warnings and errors
silentLogA shared recording logger
TEST_DESTINATIONThe default destination
runAdapterConformance(adapter, options)Runs the checks; returns { problems, quotes, steps, events }

ConformanceOptions:

FieldDescription
fixturesConformanceFixture[]: { name?, leg, quote, start?, transitions?, status?, expect?, ctx? }
errorPathsConformanceErrorPath[]: { name?, leg, quote, ctx?, skip? }. Each runs quote() against a provider that answers HTTP 400, 401, 429 or 500, or times out. quote() must throw an OpenRampException with the code and retryable of httpErrorToOpenRamp (see ERROR_PATHS). skip lists cases to leave out.
ctx() => AdapterContext, a fresh context per fixture. Default makeCtx({ fetch }).
fetchUsed by the default context. Default: a fake fetch with no routes.
webhooksConformanceWebhook[]: { name?, request: () => Request, rawBody, valid? (default true), events? }
webhookCtxDefault makeWebhookCtx({ fetch })

Per fixture, quote is the QuoteInput without leg; start is true (default), false, or extra StartInput fields; transitions is a list of { name, inputs? }; status defaults to true when the adapter has status(); expect is { start?: StateName; status?: StateName }, compared with stateFor(step).

Per fixture, the kit also checks that the quote output asset is the leg's to asset, and that the first requires_action step has a surface. An action with a SUBMIT or SURFACE_RESULT transition needs transition(). Webhook events get the same step rules.

Error caseExpected coderetryable
HTTP 400NO_QUOTES, or a more exact code (AMOUNT_TOO_LOW, AMOUNT_TOO_HIGH, REGION_UNSUPPORTED, BAD_REQUEST, PROVIDER_ERROR)any
HTTP 401PROVIDER_UNAVAILABLEfalse
HTTP 429RATE_LIMITEDtrue
HTTP 500PROVIDER_UNAVAILABLEtrue
TimeoutPROVIDER_UNAVAILABLEtrue

Environment helpers ​

ExportDescription
AdapterEnv'sandbox' | 'production': the type of the env option and of adapter.env
resolveEnv(adapter, env, legacy, fallback)The env of an adapter: env when set; else the value of a deprecated option (legacy), with a one-time warning; else fallback. Throws on a value that is not sandbox or production.
warnDeprecatedOnce(key, message)Writes a deprecation warning once per process. Returns true the first time.

Other exports ​

Logger, ScopedKV, AdapterDefinition, and the types above.

MIT licensed