feat(protocols): add SDA (Smart Deposit Address) protocol interface#46
feat(protocols): add SDA (Smart Deposit Address) protocol interface#46AlonzoRicardo wants to merge 26 commits into
Conversation
Add the generic `SdaProtocol` interface for "Smart Deposit Address" providers — services that issue a deposit address, accept a stablecoin (or native token) from a supported source chain, convert it, and deliver a chosen asset (e.g. USDT) to a chosen destination chain and address. - New `src/protocols/sda-protocol.js`: `ISdaProtocol` + abstract `SdaProtocol`, with normalized types (route, deposit address, quote, transfer) and a `getCapabilities()` descriptor so consumers can adapt to provider differences without trial-and-error. - Required core every provider implements: `getSupportedRoutes`, `createDepositAddress`. Everything else (quote, get-address, status, history, recovery, disable) is optional and capability-gated. - Re-exported from `@tetherto/wdk-wallet/protocols`. Part 1 of 3 (interface). Companion PRs: getSdaProtocol wiring in tetherto/wdk and the `sda` module type in tetherto/create-wdk-module. Generated `.d.ts` are produced by `npm run build:types`.
The published @tetherto/wdk-wallet does not yet export SdaProtocol, so CI cannot resolve the import and the test suites fail to load. Point the dependency at the wdk-wallet feat/sda-protocol branch so CI is green and the wiring is reviewable. REVERT BEFORE MERGE: bump @tetherto/wdk-wallet back to a published version that includes SdaProtocol once tetherto/wdk-wallet#46 is merged and released.
|
great addition @AlonzoRicardo I'm commenting from the angle of Candide's Forwarding Address. I think this is solid and overall very well written from a capabilities standpoint. A few things I think are missing or worth a fit. Currently, capabilities don't say who holds the funds. Relay, Rhino, and Orchestra are all operator controlled. This means the deposit address is the provider's, and recovery means "ask their backend to reindex/reactivate." A self-custodial version is a different architecture: the address is an onchain smart contract with withdrawal rights fixed in code. In our case the recipient can withdraw immediately, and a custodial withdrawer (an optional recovery wallet) can do so only after a timelock. So funds are recoverable onchain without the provider, and no single operator can unilaterally move them. Since all three reference providers (Relay, Rhino, and Orchestra) sit on the operator side, nothing in the interface currently forces the distinction. I can propose to add something along these lines to cover the self-custodial, trust minimized design:
Two gaps in
Happy to send the If I missed any other tought or feedback @sherifahmed990 please feel free to further add it |
…nterface Address Candide's review on #46 (their self-custodial Forwarding Address): - `custodyModel` ('self-custodial' | 'trusted-operator') and `clientDerivableAddress` capabilities — distinguish operator-held from on-chain self-custodial deposit addresses. - Clarify that `recovery` is provider-side missed-deposit reprocessing, NOT fund custody; `recovery: 'none'` no longer implies funds are unrecoverable (that's `custodyModel`). - Optional `getTransfersByRecipient` + `historyByRecipient` capability for providers that aggregate status by recipient across many addresses. - Optional `withdrawer` and `salt` on `SdaCreateOptions` (inputs to a deterministic CREATE2 address derivation). Validated against a 4th reference provider (self-custodial, client-derivable).
|
Thanks @Sednaoui — I've pushed an update to this PR that folds in your feedback: Added
History by recipient. Added optional Create options. Added optional Yes please, I'd love the
|
|
|
To build on Sherif’s comment, I propose WDK interface to not hardcode Candide’s derivation tuple as the universal model. Other self-custodial, destination specific providers may encode different parameters in their deposit address, depending on their recovery model, execution model, supported inputs, or routing design. To keep this generic, I can suggest writing the common capabilities, while leaving derivation inputs provider specific, like the following: Capabilities
Methods
Data
For Candide, the derivation payload is: {
recipient,
custodialWithdrawer,
destinationChainId,
salt?
}One nuance: for Candide, client derivation is mainly used as a verification, fallback, and recovery property. The normal flow is still to call forwarding_getAddress, optionally verify the returned address, then call account_activateforwardingaddress so the relayer monitors it. Deriving the address alone does not activate forwarding. |
…erface Follow-up to Candide's review (#46): generalise self-custodial support so the interface does not hard-code any provider's derivation tuple. - New `activation: 'none' | 'required' | 'ttl'` capability (address lifecycle). `recovery` no longer carries 'reactivate' — keep-alive of an expired address is now `activation: 'ttl'`. - Replace the typed `withdrawer`/`salt` create options with a generic, provider-specific `derivation` payload, echoed back on `SdaDepositAddress` so an address can be re-derived / verified / recovered later. - New optional `deriveDepositAddress` (pure client-side derivation) and `renewDepositAddress` (refresh a `ttl` activation). - `createDepositAddress` documented as returning a ready-to-receive address (activating it where the model requires). Validated with a live self-custodial reference (Candide Forwarding Address).
|
Thanks @Sednaoui — I've pushed an update to this PR that folds in your feedback: Added
History by recipient. Added optional Create options. Added optional Yes please, I'd love the
|
Replace `string | number` on every chain field with a shared `Blockchain` type alias (PR #46 review). Also commits the generated SDA `.d.ts` (previously missing) — unrelated pre-existing type drift in other protocols' declarations is left untouched.
Widen SdaLimits.min/max to `number | bigint` (PR #46 review).
The id keys status/recovery/disable operations, so a created address must always carry one (PR #46 review).
…sOptions Offset-based pagination (`skip`) is consistent with the wallet modules' get-transfers methods (PR #46 review).
Shorter, matches the wallet modules' naming (PR #46 review). Coexists with getTransfersByRecipient (by-address vs by-recipient history).
…nationChain, recipient) PR #46 review.
SDA has no config fields all providers share, so the base takes only the account; each provider declares its own config and constructor (PR #46 review).
UnsupportedOperationError is thrown by optional operations a concrete implementation deliberately doesn't support (distinct from a NotImplementedError abstract stub). AccountRequiredError is thrown when an operation needs a wallet account but none was bound at construction. Both support the SDA protocol's capability-by-behavior model (PR #46 review).
…ed capability Remove the `getCapabilities()` method and the `SdaCapabilities` typedef. Capability is now discovered by behavior: an optional operation a provider doesn't support inherits the base and throws UnsupportedOperationError, and the descriptive traits (custody / activation / route-discovery models) are documented on each provider rather than queried. Also drop the `reusableAddresses` trait — reusable vs single-use is a per-request choice via `SdaCreateOptions.reusable`, not a static protocol trait (PR #46 review; Jonathan/Davide).
createDepositAddress and deriveDepositAddress resolve the delivery destination from the bound account when `destinationAddress` is omitted; document that they throw AccountRequiredError when neither is available, so a missing account surfaces a clear error instead of a downstream TypeError (PR #46 review, #15).
…hapes
Recovery previously took an all-optional bag, so recoverDepositAddress({})
type-checked. Model it as SdaRecoverById | SdaRecoverByAddress instead — a caller
must identify the deposit by SDA id or by deposit address, and the empty call is
now a type error. Drops the unused sourceTxHash field; a provider needing other
inputs extends the relevant member (PR #46 review, #13; Davi0k approved).
The shared interface should describe behavior in provider-neutral terms; the descriptions survive a provider swap untouched. Dropped the "(Orchestra, Relay)" and "(e.g., Rhino)" asides from SdaRecoveryMode and SdaQuoteOptions (AD11).
…t class tsc's JS->.d.ts declaration emit flattens JSDoc @interface and @abstract to plain `class` (verified: regenerating swap-protocol.d.ts flips its hand-maintained `interface` / `abstract class` back to `class`). Hand-edit the SDA declarations to match the sibling protocols and the .d.ts maintenance convention (TD7/TD8).
… estimate createDepositAddress no longer accepts a pre-fetched quote — a provider whose address binds to a quote fetches one internally with a cache (the #7 conclusion: never require callers to quote up front). quoteDeposit / SdaQuote / SdaQuoteOptions stay as an optional standalone estimate (real providers like Rhino and Relay expose quote endpoints); only the quote-first coupling and the now-stale "required up front" docs are removed. .d.ts hand-edited, no build:types.
…erwise Clarify that a provider whose deposit limits are enforced in another denomination (e.g. USD) omits `limits` rather than converting into the input token's base unit, so the field never carries a mismatched unit. .d.ts hand-edited, no build:types.
- errors: add ValueError (correct type, invalid value) and NoSuchElementError (lookup miss); remove the now-unused AccountRequiredError; trim the UnsupportedOperationError description. - ISdaProtocol methods now all throw NotImplementedError; the abstract SdaProtocol keeps throwing UnsupportedOperationError for optional operations (the base default a non-supporting provider inherits). - @throws: route-discovery validation -> ValueError; create/derive missing destination -> ValueError; not-found lookups -> NoSuchElementError; standardize the UnsupportedOperationError condition text. .d.ts hand-edited, no build:types.
…face - rename types: SdaLimits->SdaDepositAddressLimits, SdaQuoteOptions->SdaDepositOptions, SdaQuote->SdaDepositQuote, SdaCreateOptions->SdaCreateDepositAddressOptions, destinationAsset->outputAsset; method getTransferStatus->getTransfer. - delete the docs-only enum typedefs (SdaRouteDiscoveryMode/SdaActivationModel/ SdaCustodyModel/SdaRecoveryMode); inline as plain text. - KISS docs: drop the "Optional:" phrasing, the descriptive-traits and abstract-class boilerplate, and the SdaRecoveryOptions "type error" tail; provider -> protocol. - abstract SdaProtocol: optional methods are concrete defaults (drop @abstract). - .d.ts regenerated to match the doc rewrite; interface/abstract keywords re-applied.
inputToken, outputAsset and fees are not populated by any reference protocol, so drop them from SdaTransfer. id + status stay required; the rest are optional since protocols/endpoints report different subsets (review comment). .d.ts hand-edited.
…racts Adds SignerError, ValueError and NoSuchElementError (verbatim from the taxonomy agreed on tetherto#46) to the core errors module and exports them, and documents them via @throws on the multisig interface contracts (propose/approve/reject/execute/quoteExecuteProposal + owner management). Interface bodies still throw NotImplementedError; @throws documents the contract implementations must honor. UnsupportedOperationError is deferred with the optional-method (abstract-base) work. Mirrors tetherto#46's agreed definitions so the shared errors module reconciles cleanly when both land.
The committed declarations had their doc comments re-wrapped independently at 120 columns, which diverged from what tsc actually emits: it inherits the source's line breaks for verbatim-copied JSDoc, and uses a flat `* ` continuation for the synthesized @Property comments. Normalize to tsc's layout so the only remaining delta from a fresh regen is the two hand-maintained keywords (interface ISdaProtocol / abstract class SdaProtocol), which makes future type-check diffs meaningful instead of noisy. Comment-only: no signature, type or member changes.
- errors: trim the ValueError description. - types/: strip the leading JSDoc @typedef blocks from the SDA declarations, so the file opens at its first declaration like every sibling .d.ts. - SdaDepositAddress: drop the `quote` field. Only a quote-bound protocol populates it, so it belongs on that protocol's own type, not the shared one. - SdaTransfer: reduce to `id` + `status`. Verified against the four providers' APIs: Candide's forwarding_getStatus returns no amounts, fees, assets or updatedAt, so those fields cannot be shared. Protocols with richer records extend the type; fields can be added back later without a breaking change.
…essOptions Apply the shared-field rule to the create options: a field belongs on the shared type only when every protocol has it. Verified against the providers' APIs: - inputToken: Relay (originCurrency) and Orchestra (sourceAsset, which binds the address to a single deposit token) both REQUIRE it; Rhino and Candide have no such parameter. Required for two, absent for two — so it belongs on their own options types, where it can be typed as required rather than shared as optional. - reusable: only Relay has a single-vs-multi-use toggle (strict). Rhino's reusePolicy is a different concept (reuse-existing vs create-new address), which the boolean cannot express; Orchestra and Candide addresses are always reusable. - refundAddress: supported by Relay (refundTo) and Rhino, absent from Orchestra's schema, and structurally N/A for self-custodial Candide. The descriptor keeps supportedInputTokens and reusable (required) — those are how a consumer learns what an address accepts and whether it is single-use. Fields can be added back later without a breaking change. .d.ts hand-edited, no build:types.
Summary
Adds the generic
SdaProtocolinterface for Smart Deposit Address (SDA) providers — services that issue a deposit address, accept a stablecoin (or native token) from a supported source chain, convert it, and deliver a chosen asset (e.g. USDT) to a chosen destination chain and address. This is the foundation that providers (Rhino, Orchestra/Flashnet, Relay, Candide, …) implement aswdk-protocol-sda-*modules and that TW consumes for the "Other Assets" receive flow.Part 1 of 3 (the interface — the bulk of the work), mirroring the earlier swidge work.
What's in it
src/protocols/sda-protocol.js—ISdaProtocol(the interface) + abstractSdaProtocol, re-exported from@tetherto/wdk-wallet/protocols.getSupportedRoutes,createDepositAddress.quoteDeposit,deriveDepositAddress,getDepositAddress,renewDepositAddress,getTransferStatus,getDepositAddressTransfers,getTransfersByRecipient,recoverDepositAddress,disableDepositAddress.getCapabilities()returns a descriptor (custody model, client-derivability, activation lifecycle, quoting, reusability, route discovery, recovery, history-by-address/recipient, status, disable, refund) so consumers adapt to provider differences without trial-and-error.Design notes
sdacategory, not swidge. Swidge executes an on-chain swap+bridge from the user's wallet (returns{hash, fee}); SDA issues a deposit address the user funds externally and the provider converts/delivers. Different lifecycle and return shape → its own category (closest sibling isfiat).NotImplementedError;getCapabilities()advertises what's real.custodyModeldistinguishes operator-held addresses from on-chain self-custodial ones;clientDerivableAddressflags deterministically-derivable addresses;activation(none/required/ttl) captures the address lifecycle.recoveryis scoped to provider-side missed-deposit reprocessing only — keep-alive of an expired address isactivation: 'ttl'.derivationpayload (passed through, echoed back onSdaDepositAddress), withderiveDepositAddress(pure client-side) separated fromcreateDepositAddress(which also activates).Validation
Pressure-tested against four structurally different providers spanning both custody models (local throwaway modules, not part of this PR). The Candide reference is validated live against Candide's alpha forwarding API (
forwarding_getAddress/account_activateForwardingAddress/forwarding_getStatus). Every optional method is implemented by ≥1 provider and skipped by ≥1, and every capability takes multiple values:custodyModelclientDerivableAddressactivationcreateDepositAddressderiveDepositAddressrenewDepositAddressquoteRequired(usesquote)getTransferStatusgetDepositAddressgetDepositAddressTransfersgetTransfersByRecipientrecoverydisableDepositAddressEach provider also passes a full
registerProtocol → getSdaProtocol → methodround-trip via the WDK.Notes
.d.tsare produced bynpm run build:types.Related PRs
This is one of a 3-PR set:
SdaProtocolinterface (this PR)getSdaProtocolsupport: feat: add SDA protocol support (getSdaProtocol) wdk#73sdamodule type + template: feat: add sda module type and template create-wdk-module#14