Rhino.fi SDA Usage
Discover deposit routes, request Rhino.fi Smart Deposit Addresses, and reconcile their transfer history.
Community modules are developed and maintained independently by third-party contributors.
Tether and the WDK Team do not endorse or assume responsibility for their code, security, or maintenance. Use your own judgment and proceed at your own risk.
This guide shows how to install and configure an accountless protocol, discover a route, estimate a deposit, create a deposit address, track deposits, pause or resume an address, register with WDK, and handle errors. See support options if you need help with the integration.
Install and Configure
Before starting, obtain a Rhino.fi project SECRET_ API key with SDA access. This server-side guide includes address lookup, history, and pause operations that require it. Prepare an EVM depositor address on Arbitrum and a destination address on Ethereum that you control or have independently verified. The example checks live support before using that pair.
Install the SDA package for the accountless flow:
npm install @rhino.fi/wdk-protocol-sda-rhinofi@1.2.0Create an accountless RhinofiProtocol and load the confirmed addresses from your server environment:
import RhinofiProtocol, {
NoSuchElementError,
ProviderErrorReason,
SdaExecutionError
} from '@rhino.fi/wdk-protocol-sda-rhinofi'
const apiKey = process.env.RHINO_API_KEY
if (!apiKey) throw new Error('Set RHINO_API_KEY')
function confirmedEvmAddress(name) {
const address = process.env[name]
if (!address || !/^0x[0-9a-fA-F]{40}$/.test(address) || /^0x0{40}$/i.test(address)) {
throw new Error(`Set ${name} to a confirmed EVM address`)
}
return address
}
const depositor = confirmedEvmAddress('RHINO_DEPOSITOR_ADDRESS')
const destinationAddress = confirmedEvmAddress('RHINO_DESTINATION_ADDRESS')
const sourceChain = 'ARBITRUM'
const destinationChain = 'ETHEREUM'
const expectedInputTokenAddress = '0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9'
const expectedOutputTokenAddress = '0xdac17f958d2ee523a2206206994597c13d831ec7'
const expectedTokenDecimals = 6
const matchesAddress = (actual, expected) =>
typeof actual === 'string' && actual.toLowerCase() === expected.toLowerCase()
const sda = new RhinofiProtocol(undefined, { apiKey })Address syntax does not establish ownership or the intended chain. Confirm both addresses before continuing. Keep secret keys on your server; see API Access.
For TypeScript, import option and result types separately from the JavaScript classes and errors:
import type {
RhinofiCreateDepositAddressOptions,
RhinofiQuoteDepositOptions,
RhinofiSdaDepositAddress,
RhinofiSdaTransfer
} from '@rhino.fi/wdk-protocol-sda-rhinofi'Discover a Route
Use getSupportedRoutes() to check whether USD₮0 is accepted on Arbitrum and USD₮ is delivered on Ethereum:
const routes = await sda.getSupportedRoutes({
sourceChain,
sourceToken: 'USDT',
destinationChain,
outputAsset: 'USDT'
})
const route = routes.find(item =>
item.sourceChains.includes(sourceChain) && item.destinationChain === destinationChain
)
const inputToken = route?.inputTokens.find(token =>
token.token === 'USDT' && matchesAddress(token.address, expectedInputTokenAddress)
)
if (!route || !inputToken ||
inputToken.decimals !== expectedTokenDecimals ||
!matchesAddress(route.outputAsset?.address, expectedOutputTokenAddress) ||
route.outputAsset?.decimals !== expectedTokenDecimals) {
throw new Error('The requested Tether route is unavailable or its token details differ')
}
console.log(inputToken.decimals)
console.log(inputToken.minDepositLimitUsd, inputToken.maxDepositLimitUsd)The example uses Rhino.fi's USDT machine identifier, which its 1.2.0 README uses for Arbitrum. The checks use the official Arbitrum USD₮0 deployment, Ethereum USD₮ contract, and six-decimal token specification. Stop if the provider returns different or missing token details; a ticker alone does not identify the asset. Preserve the returned identifier and decimals in subsequent calls. Deposit bounds are USD values, not token base units. Refresh discovery when presenting a route; an earlier response does not reserve it. Passing sourceChain avoids querying every deposit-enabled source chain.
Estimate a Deposit (optional)
Use quoteDeposit() to estimate the output and fees for 100 USD₮0, expressed using the discovered source-token decimals:
const inputAmount = 100n * 10n ** BigInt(inputToken.decimals)
const quote = await sda.quoteDeposit({
sourceChain,
inputToken: inputToken.token,
destinationChain,
outputAsset: 'USDT',
inputAmount,
depositor,
destinationAddress
})
console.log(quote.outputAmount, quote.fees)The amount must fit the provider's current USD deposit limits. Both addresses are required even for a protocol bound to an account. The provider prices the actual deposit when funds arrive; this estimate contains no reusable quote ID or expiry and does not cap later fees or guarantee this output amount.
Create a Deposit Address
- Confirm the destination chain and address, accepted token, USD limits, and indicative fees with the user.
- Request the address using
createDepositAddress(). - Store the returned descriptor and the confirmed destination together before showing funding instructions.
The following call creates or reuses a live provider deposit address. It does not transfer funds. Only fund it with a token and source chain accepted by its current descriptor. The depositor's ordinary wallet transfer is a separate action outside this module.
Request an address using the same confirmed destination as the estimate:
const deposits = await sda.createDepositAddress({
sourceChains: [sourceChain],
destinationChain,
outputAsset: 'USDT',
destinationAddress,
reusePolicy: 'reuse-existing'
})
const deposit = deposits.find(item =>
item.sourceChains.includes(sourceChain) &&
item.destinationChain === destinationChain &&
matchesAddress(item.destinationAddress, destinationAddress) &&
matchesAddress(item.outputAsset?.address, expectedOutputTokenAddress) &&
item.outputAsset?.decimals === expectedTokenDecimals
)
if (!deposit) throw new Error('No matching deposit address returned for the selected route')
if (deposit.isActive !== true || deposit.isPaused !== false) {
throw new Error('Deposit address is inactive, paused, or has incomplete lifecycle data')
}
const acceptedToken = deposit.supportedInputTokens.find(token =>
token.token === inputToken.token &&
matchesAddress(token.address, expectedInputTokenAddress) &&
token.decimals === expectedTokenDecimals
)
if (!acceptedToken) throw new Error('The address does not currently accept the selected token')
console.log(deposit.address, deposit.id, acceptedToken)
console.log(acceptedToken.minDepositLimitUsd, acceptedToken.maxDepositLimitUsd)Use the created descriptor's token and USD limits in the funding instructions. Creation includes destinationAddress even with a standard key: the provider's returned value takes precedence, and an omitted value is filled from the request or bound account. That fallback does not independently confirm the provider's stored recipient; retain your confirmed creation destination and the checks above.
Refresh and validate the descriptor with getDepositAddress() before a later deposit. Lookup requires a SECRET_ key with destination access and fails with SdaExecutionError (reason: FORBIDDEN) if the destination is withheld. Stop on a lookup failure or a descriptor that no longer matches the selected route. Keep deposit.id verbatim for lookups and lifecycle actions. See Address Creation Options for labels, webhooks, refunds, and asset fallback.
For a Stellar descriptor containing chainMetadata, a sender that cannot use the muxed address must send to baseAddress with the exact memoId. Omitting the memo prevents deposit attribution. Read these values from the descriptor rather than constructing them.
Track Deposits
Configure a project webhook in the Rhino.fi Console, or pass an approved webhookUrl when creating an address. Verify events using the provider's signature-verification instructions before trusting their contents. The SDA module exposes history methods for reconciliation; it does not verify or receive webhook events.
Use getTransfers() to read the last 31 days for the address on its source chain:
const transfers = await sda.getTransfers(deposit.address, {
sourceChain,
limit: 20,
skip: 0
})
for (const transfer of transfers) {
console.log(transfer.id, transfer.status, transfer.txHash)
}For an older period, pass valid fromTimestamp and toTimestamp dates spanning no more than 31 days. Filtering and pagination apply within the fetched window; increasing skip does not fetch older periods.
Use getTransfer() with an identifier returned by history:
if (transfers.length > 0) {
const transfer = await sda.getTransfer(transfers[0].id)
console.log(transfer.status)
}getTransfer() searches address history within the same default window. Supply the original window when looking up an older transfer. A missing result can mean the transfer is outside the window.
Interpret each transfer independently. Rejected deposits report refund-pending, and polling does not change that status to refunded. Use the provider's SDA_REFUND_COMPLETED event for refund completion. Unsupported or out-of-range deposits can require the provider's Account Management team to arrange recovery; do not assume every rejected transfer is automatically refunded. Unknown provider states map to pending; see Transfer Status.
Pause or Resume an Address
Use disableDepositAddress() to pause acceptance for the address and chain identified by deposit.id:
await sda.disableDepositAddress(deposit.id)Deposits arriving during the pause are rejected. Refund handling can require provider intervention. Pausing does not establish that an existing transfer has completed or that its refund was issued.
Use enableDepositAddress() to resume acceptance:
await sda.enableDepositAddress(deposit.id)Register with WDK (optional)
Use this alternative when your application already manages the destination account through WDK. Install all three packages imported by the account-bound example:
npm install @rhino.fi/wdk-protocol-sda-rhinofi@1.2.0 @tetherto/wdk@1.0.0-beta.19 @tetherto/wdk-wallet-evm@1.0.0-beta.20- Register the destination wallet and the SDA protocol with
registerProtocol(). - Retrieve the destination account and its SDA protocol.
- Dispose the WDK wallet after using it.
Retrieve the registered provider with getSdaProtocol(); the example only reads the account's address:
import WDK from '@tetherto/wdk'
import WalletManagerEvm from '@tetherto/wdk-wallet-evm'
import RhinofiProtocol from '@rhino.fi/wdk-protocol-sda-rhinofi'
const seed = process.env.WDK_SEED_PHRASE
const apiKey = process.env.RHINO_API_KEY
if (!seed || !WDK.isValidSeed(seed) || !apiKey) {
throw new Error('Set a valid WDK_SEED_PHRASE and RHINO_API_KEY')
}
const wdk = new WDK(seed)
try {
wdk.registerWallet('ethereum', WalletManagerEvm, {})
wdk.registerProtocol('ethereum', 'rhinofi-sda', RhinofiProtocol, { apiKey })
const account = await wdk.getAccount('ethereum', 0)
const accountSda = account.getSdaProtocol('rhinofi-sda')
const destinationAddress = await account.getAddress()
console.log(destinationAddress, typeof accountSda.createDepositAddress)
} finally {
wdk.dispose()
}The bound account supplies the default creation destination when omitted. Always pass destinationChain explicitly and match it to that account. The returned core interface is ISdaProtocol. The pinned JavaScript flow works; see TypeScript Compatibility for its separate EVM wallet-manager declaration mismatch. Rhino.fi's provider assignment and SDA registration checks pass with WDK beta.19. A direct RhinofiProtocol instance exposes provider-specific options, return fields, and enableDepositAddress().
Handle Errors
Use the imported SdaExecutionError and WDK NoSuchElementError classes to classify request failures and missing addresses:
try {
await sda.getDepositAddress(deposit.id)
} catch (error) {
if (error instanceof SdaExecutionError) {
if (error.reason === ProviderErrorReason.FORBIDDEN) {
console.error('Check SECRET_ key permissions and destination access')
} else {
console.error('Rhino.fi request failed', error.code)
}
} else if (error instanceof NoSuchElementError) {
console.error('Deposit address not found')
} else {
throw error
}
}SdaExecutionError.code and its WDK reason are optional. Local numeric conversions can also throw native errors. Validate caller inputs and avoid logging credentials or unfiltered provider error details. After an uncertain address-creation response, reconcile with your stored provider records before requesting another address.
Next Steps
Configuration
Review authentication, caching, account defaults, and creation extras.
API Reference
Review exact methods, returned descriptors, status mapping, and errors.