wdk-protocol-swidge-orchestra
wdk-protocol-swidge-orchestra is a WDK SwidgeProtocol for moving between BTC on Spark or Bitcoin L1 and USDT on Ethereum, Tron, Arbitrum, Solana, BSC, Optimism, Plasma, Polygon, TON, and every WDK-executable route returned by Orchestra. It also supports destination Lightning payments where Orchestra routes exist.
WDK owns wallet accounts, key material, and transaction signing. Orchestra owns quote routing, deposit addresses, order state, and settlement. The host wallet owns durable local or backend persistence.
WDK account -> Orchestra protocol -> Flashnet Orchestra API
| | |
| signs payment | submits tx id | routes and settles order
| | |
+---- app persists serializable state --+
The package does not run a database, custody keys, poll chains directly, or hide money-moving state transitions from the app.
USDT Route Coverage
Use the live route matrix as the source of truth:
Representative BTC and USDT routes:
const routes = [
"spark:BTC -> tron:USDT",
"bitcoin:BTC -> ethereum:USDT",
"arbitrum:USDT -> spark:BTC",
"bsc:USDT -> spark:BTC",
"solana:USDT -> lightning:BTC",
"polygon:USDT -> bitcoin:BTC"
]The package also exposes WDK discovery methods:
const chains = await orchestra.getSupportedChains()
const tokens = await orchestra.getSupportedTokens({
fromChain: "spark",
toChain: "tron"
})Use discovery for wallet UI. Use the route matrix when you need the exact source and destination pairs Orchestra can execute at that moment. For this package's standard WDK execution flow, do not expose source Lightning routes.
Install
npm install wdk-protocol-swidge-orchestra @tetherto/wdk-wallet@1.0.0-beta.11Install the WDK wallet packages for the chains you support:
npm install @tetherto/wdk@1.0.0-beta.12 @tetherto/wdk-wallet-spark@1.0.0-beta.21 @tetherto/wdk-wallet-btc@1.0.0-beta.10 @tetherto/wdk-wallet-evm@1.0.0-beta.14WDK Interface
Orchestra extends SwidgeProtocol from @tetherto/wdk-wallet@1.0.0-beta.11.
Implemented methods:
quoteSwidge(options)swidge(options, config?)getSwidgeStatus(id, options?)getSupportedChains()getSupportedTokens(options?)
WDK's SwidgeProtocol base class delegates quoteSwap, swap, quoteBridge, and bridge to quoteSwidge and swidge. This package does not override those methods.
Current WDK beta note: the protocol class conforms to SwidgeProtocol. If the WDK manager in your app has not yet added Swidge registration helpers, construct the protocol from the source account directly as shown below.
Create A Protocol
Create one Orchestra instance per source wallet account. Set sourceChain explicitly because WDK accounts do not always expose a canonical chain id to protocol constructors.
import WDK from "@tetherto/wdk"
import WalletManagerBtc from "@tetherto/wdk-wallet-btc"
import WalletManagerEvm from "@tetherto/wdk-wallet-evm"
import WalletManagerSpark from "@tetherto/wdk-wallet-spark"
import Orchestra from "wdk-protocol-swidge-orchestra"
const wdk = new WDK(seedPhrase)
.registerWallet("spark", WalletManagerSpark, {
network: "MAINNET",
syncAndRetry: true
})
.registerWallet("bitcoin", WalletManagerBtc, {
network: "bitcoin",
client: {
type: "electrum",
clientConfig: {
host: "electrum.blockstream.info",
port: 50001
}
}
})
.registerWallet("arbitrum", WalletManagerEvm, {
chainId: 42161,
provider: process.env.ARBITRUM_RPC_URL
})
const spark = await wdk.getAccount("spark", 0)
const orchestra = new Orchestra(spark, {
sourceChain: "spark",
apiKey: process.env.FLASHNET_API_KEY,
baseUrl: "https://orchestration.flashnet.xyz"
})EVM token sources need token contract addresses. Common USDT source addresses are built in, but production wallets should pass their own allowlist.
const arbitrum = await wdk.getAccount("arbitrum", 0)
const orchestra = new Orchestra(arbitrum, {
sourceChain: "arbitrum",
apiKey: process.env.FLASHNET_API_KEY,
sourceTokenAddresses: {
"arbitrum:USDT": "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9"
}
})Quote
quoteSwidge() is side-effect-free. It calls Orchestra's estimate endpoint and does not reserve a deposit address.
Treat the result as indicative UI data. It does not bind execution, lock a rate, or reserve the returned amounts, fees, or expiry. swidge() creates a fresh binding Orchestra quote through prepareSwap(), so wallet UIs should not assume a prior quoteSwidge() result is reused.
const quote = await orchestra.quoteSwidge({
fromToken: "spark:BTC",
toToken: "tron:USDT",
fromTokenAmount: 7116n,
recipient: "TRecipient...",
slippage: 0.01
})
console.log(quote.fromTokenAmount)
console.log(quote.toTokenAmount)
console.log(quote.toTokenAmountMin)
console.log(quote.fees)Use smallest units:
| Asset | Unit |
|---|---|
| BTC | sats |
| USDT | 6-decimal token units |
| EVM gas asset | wei |
Execute With WDK Swidge
swidge() creates an Orchestra quote, sends the source payment from the WDK account to the quote deposit address, submits the transaction id to Orchestra, and returns the Orchestra order id.
const result = await orchestra.swidge({
fromToken: "spark:BTC",
toToken: "tron:USDT",
fromTokenAmount: 7116n,
recipient: "TRecipient..."
}, {
maxNetworkFeeBps: 20n,
maxProtocolFeeBps: 100n
})
console.log(result.id)
console.log(result.hash)
console.log(result.transactions)There is a recovery gap after the source payment is sent and before Orchestra accepts the transaction id. If the app exits in that window, funds have moved but the order may not yet be linked to the source transaction.
Use this one-call path only when the host app has recovery around the call. Production wallets should prefer prepareSwap() -> persist their own saveSwap state -> executeSwapIntent(). If they use swidge(), they should persist the state passed to every onStateChange callback and persist OrchestraSubmitError.state before retrying.
Production Flow
Use prepareSwap() and executeSwapIntent() when funds are at risk. The split flow gives the host wallet a durable point between quote creation and source payment.
prepareSwap()creates an Orchestra quote and reserves a deposit address.- The app persists the returned intent.
executeSwapIntent()sends the source payment and submits the transfer id.- The app persists the submitted state.
- The app tracks the order with
getOrderStatus(),getSwidgeStatus(),waitForCompletion(), or SSE.
saveSwap and saveOrderStatus are not exported by this package. They are placeholders for your implementation. Back them with your own database, encrypted local storage, or backend API before moving real funds.
const intent = await orchestra.prepareSwap({
fromToken: "spark:BTC",
toToken: "tron:USDT",
fromTokenAmount: 7116n,
recipient: "TRecipient..."
})
await saveSwap(intent)
const submitted = await orchestra.executeSwapIntent(intent)
await saveSwap(submitted)
const finalStatus = await orchestra.waitForCompletion(submitted, {
onStatus: async (status) => {
await saveOrderStatus(status)
}
})Persist the full object returned by every state transition. Do not store only the order id. Recovery may need the quote id, deposit address, source tx hash, read token, source chain, and submit idempotency key.
Spark BTC To USDT
Spark signs the BTC transfer. Orchestra settles USDT on the destination chain.
const spark = await wdk.getAccount("spark", 0)
const orchestra = new Orchestra(spark, {
sourceChain: "spark",
apiKey
})
const intent = await orchestra.prepareSwap({
fromToken: "spark:BTC",
toToken: "tron:USDT",
fromTokenAmount: 7116n,
recipient: "TRecipient..."
})
await saveSwap(intent)
const submitted = await orchestra.executeSwapIntent(intent)
await saveSwap(submitted)The submitted state includes the Spark source transfer id, source wallet network fee, Orchestra order id, and read token when using a scoped client key.
console.log(submitted.sourceTxHash)
console.log(submitted.sourceNetworkFee)
console.log(submitted.orderId)
console.log(submitted.readToken)USDT To Spark BTC
EVM token sources use WDK transfer({ token, recipient, amount }). The source account needs native gas for its chain.
const bsc = await wdk.getAccount("bsc", 0)
const spark = await wdk.getAccount("spark", 0)
const orchestra = new Orchestra(bsc, {
sourceChain: "bsc",
apiKey,
sourceTokenAddresses: {
"bsc:USDT": "0x55d398326f99059ff775485246999027b3197955"
}
})
const intent = await orchestra.prepareSwap({
fromToken: "bsc:USDT",
toToken: "spark:BTC",
fromTokenAmount: 5_000_000n,
recipient: await spark.getAddress()
})Bitcoin L1
Bitcoin L1 can be the source or destination.
L1 BTC Source
const bitcoin = await wdk.getAccount("bitcoin", 0)
const spark = await wdk.getAccount("spark", 0)
const orchestra = new Orchestra(bitcoin, {
sourceChain: "bitcoin",
apiKey
})
const intent = await orchestra.prepareSwap({
fromToken: "bitcoin:BTC",
toToken: "spark:BTC",
fromTokenAmount: 100000n,
recipient: await spark.getAddress()
})
await saveSwap(intent)
const submitted = await orchestra.executeSwapIntent(intent, {
feeRate: 12n,
confirmationTarget: 2
})For Bitcoin sources, the package submits bitcoinTxid to Orchestra. It retries tx_not_found and vout_not_found submit responses with the same idempotency key because a newly broadcast Bitcoin transaction may need time to propagate.
L1 BTC Destination
const spark = await wdk.getAccount("spark", 0)
const bitcoin = await wdk.getAccount("bitcoin", 0)
const orchestra = new Orchestra(spark, {
sourceChain: "spark",
apiKey
})
const intent = await orchestra.prepareSwap({
fromToken: "spark:BTC",
toToken: "bitcoin:BTC",
fromTokenAmount: 100000n,
recipient: await bitcoin.getAddress()
})Pass recipient for cross-account routes. A protocol constructed on a Spark account cannot infer the user's Bitcoin receive address.
Lightning
Orchestra supports destination Lightning routes such as bsc:USDT -> lightning:BTC and solana:USDT -> lightning:BTC. A wallet can pay a BOLT11 invoice or Lightning Address using USDT from a supported chain where the route exists.
const bsc = await wdk.getAccount("bsc", 0)
const orchestra = new Orchestra(bsc, {
sourceChain: "bsc",
apiKey
})
const intent = await orchestra.prepareSwap({
fromToken: "bsc:USDT",
toToken: "lightning:BTC",
fromTokenAmount: 5_000_000n,
recipient: bolt11Invoice,
refundChain: "bsc",
refundAddress: await bsc.getAddress()
})Current API behavior: Orchestra asks for refundAddress on Lightning destinations. The package forwards refundChain and refundAddress; it does not enforce the rule locally. If Orchestra removes that API requirement, this package does not need a public API change.
Lightning as a source is not supported through this package's standard swidge() or executeSwapIntent() flow. Those methods execute WDK account sends and submit source transaction ids. They do not implement an Orchestra receive-request-id path for a user-paid Lightning invoice.
Auth
The package supports backend keys and scoped client keys.
Backend key:
const orchestra = new Orchestra(account, {
sourceChain: "spark",
apiKey: process.env.FLASHNET_API_KEY,
baseUrl: "https://orchestration.flashnet.xyz"
})Scoped client key:
const orchestra = new Orchestra(account, {
sourceChain: "spark",
apiKey: process.env.FLASHNET_CLIENT_KEY,
authMode: "client"
})Scoped client-key submissions return a readToken. The package stores it on the submitted state and uses it for status reads when the state object is passed back in.
const submitted = await orchestra.executeSwapIntent(intent)
await orchestra.getOrderStatus({
orderId: submitted.orderId,
readToken: submitted.readToken
})Backend proxy integrations can provide auth headers per request:
const orchestra = new Orchestra(account, {
sourceChain: "spark",
baseUrl: "https://your-api.example.com/orchestra",
getAuthHeaders: async () => ({
Authorization: `Bearer ${await getSessionToken()}`
})
})Direct browser SSE needs a URL token because EventSource cannot set headers. Admin keys are never sent as URL tokens. For admin-key integrations, proxy SSE from your backend or provide a scoped SSE token through sseToken or getSseToken.
const subscription = orchestra.subscribeOrder(submitted, {
onStatus: (status) => {
console.log(status)
},
onError: (err) => {
console.error(err)
}
})
subscription.close()State And Recovery
The app must persist every state transition that can affect funds.
const orchestra = new Orchestra(account, {
sourceChain: "spark",
apiKey,
onStateChange: async (event, state) => {
await saveSwap(state)
}
})saveSwap must be your own implementation. It should write the complete state object durably enough that a process crash, browser tab close, mobile app restart, or backend deploy can resume from the latest known state.
State transitions:
| Event | Meaning |
|---|---|
intent_created |
Quote exists and has a deposit address. No source funds moved. |
source_payment_started |
The package is about to broadcast or send the source payment. Persist before this callback returns. |
source_payment_sent |
Source payment returned a transaction id. |
submitted |
Orchestra accepted the source transaction and created or updated the order. |
Recovery uses the most complete state you have:
const next = await orchestra.resumeSwap(savedState)Rules:
- If
orderIdexists,resumeSwap()reads order status. - If
sourceTxHashexists,resumeSwap()submits or re-submits the source transaction id. - If only the intent exists,
resumeSwap()refuses to send a fresh payment unlessallowNewSourcePayment: trueis set.
Use allowNewSourcePayment: true only after checking wallet history for a prior payment to the quote deposit address.
await orchestra.resumeSwap(intentOnlyState, {
allowNewSourcePayment: true
})If submit fails after the source payment was sent, the thrown error is OrchestraSubmitError. Persist error.state before retrying.
try {
const submitted = await orchestra.executeSwapIntent(intent)
await saveSwap(submitted)
return submitted
} catch (err) {
if (err.name !== "OrchestraSubmitError") throw err
await saveSwap(err.state)
return await orchestra.resumeSwap(err.state)
}Error Types
All package-specific errors extend OrchestraError.
| Error | When it is thrown | Useful fields |
|---|---|---|
OrchestraError |
Base class for package-specific failures. | code, details |
OrchestraApiError |
Orchestra returns an API error or an invalid API response. | code, status, details |
OrchestraStateError |
Input state is incomplete, unsafe to resume, expired, or not compatible with the requested source payment. | code, details |
OrchestraSubmitError |
The source payment was sent, but submit, post-submit validation, or post-submit state persistence failed. Persist state before retrying. |
state, cause |
OrchestraTimeoutError |
An HTTP request or wait operation exceeds its timeout. | code, details |
Status Mapping
getSwidgeStatus() maps Orchestra order status into the WDK Swidge status enum.
| Orchestra status | WDK Swidge status |
|---|---|
processing or unknown in-flight state |
pending |
completed |
completed |
failed |
failed |
unfulfilled |
failed |
expired |
expired |
refunded |
refunded |
Fee Mapping
Orchestra quote fees are route fees. Source wallet fees are only known after WDK sends the source payment.
| Source | WDK fee type | Included | When known |
|---|---|---|---|
Orchestra feeAmount or totalFeeAmount |
protocol |
Yes | Quote and execution |
WDK source payment sourceNetworkFee |
network |
No | After source payment |
sourceNetworkFee is the fee returned by WDK when this package sends the source transaction. It is not an Orchestra fee.
Asset References
Use chain-qualified asset references in app code:
fromToken: "bsc:USDT"
toToken: "spark:BTC"Unqualified assets use the protocol sourceChain. Prefer explicit chain prefixes in wallet UI code.
Spark tokens other than BTC need token identifiers:
const orchestra = new Orchestra(sparkAccount, {
sourceChain: "spark",
apiKey,
sparkTokenIdentifiers: {
USDB: "btkn1..."
}
})EVM tokens need token contract addresses:
const orchestra = new Orchestra(bscAccount, {
sourceChain: "bsc",
apiKey,
sourceTokenAddresses: {
"bsc:USDT": "0x55d398326f99059ff775485246999027b3197955"
}
})Live Test Harness
The repository includes a local harness for funded smoke tests. These commands move mainnet funds by default. The harness uses Arbitrum because gas cost is low and the WDK EVM wallet works with a public RPC. The route matrix remains the source of truth for supported chains.
The harness creates a random mnemonic, stores it in .orchestra-live.env, prints funding addresses, and stores order state in .orchestra-live-state/. Both paths are ignored by git.
npm install
npm run live:initSet FLASHNET_API_KEY in .orchestra-live.env or pass it as an environment variable.
npm run live:addressesSpark BTC to Arbitrum USDT:
npm run live:quote -- --direction spark-btc-to-arbitrum-usdt --amount 7116
npm run live:prepare -- --direction spark-btc-to-arbitrum-usdt --amount 7116 --out .orchestra-live-state/spark-to-arb.json
npm run live:execute -- --file .orchestra-live-state/spark-to-arb.json --yes
npm run live:wait -- --file .orchestra-live-state/spark-to-arb.json --timeoutMs 7200000Arbitrum USDT to Spark BTC:
npm run live:quote -- --direction arbitrum-usdt-to-spark-btc --amount 5466162 --to spark1...
npm run live:prepare -- --direction arbitrum-usdt-to-spark-btc --amount 5466162 --to spark1... --out .orchestra-live-state/arb-to-spark.json
npm run live:execute -- --file .orchestra-live-state/arb-to-spark.json --yes
npm run live:wait -- --file .orchestra-live-state/arb-to-spark.json --timeoutMs 7200000Bitcoin L1 to Arbitrum USDT:
npm run live:quote -- --direction btc-to-arbitrum-usdt --amount 10000
npm run live:prepare -- --direction btc-to-arbitrum-usdt --amount 10000 --out .orchestra-live-state/btc-to-arb.json
npm run live:execute -- --file .orchestra-live-state/btc-to-arb.json --feeRate 12 --confirmationTarget 2 --yes
npm run live:wait -- --file .orchestra-live-state/btc-to-arb.json --timeoutMs 7200000Arbitrum USDT to Bitcoin L1:
npm run live:quote -- --direction arbitrum-usdt-to-btc --amount 5000000
npm run live:prepare -- --direction arbitrum-usdt-to-btc --amount 5000000 --out .orchestra-live-state/arb-to-btc.json
npm run live:execute -- --file .orchestra-live-state/arb-to-btc.json --yes
npm run live:wait -- --file .orchestra-live-state/arb-to-btc.json --timeoutMs 7200000EVM source tests require native gas on the source account.
API Surface
class Orchestra extends SwidgeProtocol {
quoteSwidge(options): Promise<SwidgeQuote>
swidge(options, config?): Promise<SwidgeResult>
getSwidgeStatus(id, options?): Promise<SwidgeStatusResult>
getSupportedChains(): Promise<SwidgeSupportedChain[]>
getSupportedTokens(options?): Promise<SwidgeSupportedToken[]>
prepareSwap(options, requestOptions?): Promise<OrchestraSwapIntent>
executeSwapIntent(intentOrState, options?): Promise<OrchestraSwapState>
submitSourceTx(
intentOrState,
sourceTxHash,
options?
): Promise<OrchestraSwapState>
resumeSwap(state, options?): Promise<OrchestraSwapState | StatusResponse>
getOrderStatus(target): Promise<StatusResponse>
waitForCompletion(target, options?): Promise<StatusResponse>
subscribeOrder(target, callbacks, options?): OrderSubscription
}Development
npm install
npm test
npm run lint
npm run build:types
npm pack --dry-runSupport
For package support, reach out to info@flashnet.xyz.
Security
Report vulnerabilities through GitHub Security Advisories. Do not open a public issue for a vulnerability. See SECURITY.md.
License
Apache-2.0