@securitize/solana-bridge-sdk
v0.2.0
Published
Securitize Bridge Solana program SDK
Keywords
Readme
@securitize/solana-bridge-sdk
TypeScript SDK for the Securitize DS-token bridge on Solana. Wraps the securitize_bridge Anchor
program: instruction builders, PDA helpers, executor-quote utilities, and a high-level
SecuritizeBridgeClient class that handles ALT resolution, signing, and retries.
yarn add @securitize/solana-bridge-sdkEntry points
SecuritizeBridgeClient— high-level async client (signs, submits, syncs state).SecuritizeBridgeClient.instructions— stateless instruction builders returningTransactionInstruction[]. Use when you compose your own transaction / submission flow.SecuritizeBridgeClient.transactions— same builders wrapped as legacyTransactionfactories.- PDA helpers (
bridgeConfigPda,bridgeAddressPda,sentMessagePda, …) fromsrc/utils.ts. - Executor-quote utilities (
fetchExecutionQuote,parsePayeeFromSignedQuote) fromsrc/executor-quote.ts.
Required payer SOL balance
Reserve ≥ 0.01 SOL above wormhole_fee + exec_amount when calling
bridge_ds_tokens. The on-chain handler only checks that lower bound; a
successful transaction additionally needs rent for the posted Wormhole message
account, a one-time rent for the per-mint sequence tracker on the first send,
the payer rent-exempt minimum, and tx / priority fees.
Concurrency semantics (outbound bridging)
Outbound bridgeDsTokens performs a Wormhole post_message CPI. Wormhole maintains one
sequence-tracker account per emitter (per-mint in our case). Every successful outbound send
mutates that account, and Solana runtime serializes writes to it.
Two consequences follow:
- Per-mint throughput ceiling. Concurrent sends for the same asset mint do not run in parallel; they queue. Effective throughput is bounded by single-account contention (roughly one confirmed send per slot in the worst case).
- Race window on pre-built transactions. The
wormhole_messageaccount is a PDA derived from the current sequence value. The SDK reads that value from RPC to derive the PDA. If a competing transaction lands between the read and the on-chain check, the program reverts withInvalidMessage(Anchor error code 6007).
This is inherent to Wormhole + Solana — it is not bridge-specific and cannot be eliminated on-chain. The SDK absorbs it with automatic retries.
Default retry behavior
SecuritizeBridgeClient.bridgeDsTokens(params) automatically retries on InvalidMessage:
| Option | Default | Effect |
| ------------------------------------- | -------- | -------------------------------------------------------------------------------------------------- |
| retry.maxAttempts | 3 | Total attempts including the first. Set to 1 to disable retries. |
| retry.retryableCodes | [6007] | Only InvalidMessage by default. Other errors (auth, balance, paused, …) are not retried. |
| retry.enablePriorityFeeAfterAttempt | 2 | Attempt number (1-indexed) from which withPriorityFee: true is forced. |
| retry.refreshExecutorQuote | unset | Optional async callback; when set, invoked before each retry to refresh short-TTL executor quotes. |
| retry.onRetry | unset | Hook ({ attempt, nextAttempt, errorCode }) => void for logging / metrics. |
Each retry rebuilds the instruction set from scratch — that re-fetches the current sequence
value and re-derives wormhole_message, closing the race window.
Refreshing short-TTL executor quotes
If you build the signed executor quote far in advance of submitting (minutes), the quote may
expire between retries. Pass refreshExecutorQuote so that each retry picks up a fresh quote:
const refreshExecutorQuote = async () => {
const quote = await client.getExecutionQuote({ targetChain });
return {
signedQuoteBytes: quote.signedQuoteBytes,
execAmount: quote.estimatedCost,
};
};
await client.bridgeDsTokens({
targetChain,
amount,
recipient,
execAmount,
signedQuoteBytes,
investor,
revokeTokensAccounts,
executorAccounts,
retry: { refreshExecutorQuote },
});Building your own submission flow
The retry loop lives in SecuritizeBridgeClient.bridgeDsTokens. If you call
SecuritizeBridgeClient.instructions.bridgeDsTokens(...) directly and submit via your own
transport, you must implement equivalent retry logic yourself:
- Catch the send/confirm error and inspect it via
extractAnchorErrorCode(err)(exported fromsrc/utils.ts). It matches both native Anchor logs (Error Number: N), runtime hex logs (Custom program error: 0xNNNN), and stringifiedTransactionError({"Custom":N}). - Compare the code against
BRIDGE_ERROR_CODE.INVALID_MESSAGE(6007, exported fromsrc/constants.ts). - On match, rebuild the instruction set (which re-reads the sequence) and resubmit. Add a priority-fee compute-budget ix on retry to outbid contending senders.
Griefing failure mode
An adversary can, for a cost, induce InvalidMessage reverts on a victim by landing their own
bridge_ds_tokens transaction between the victim's RPC read and on-chain execution. The attack
does not cause loss of funds (all state changes happen after the wormhole_message check)
and is bounded by the attacker's willingness to pay Wormhole + executor fees and burn bridge-minimum
amounts via the RBAC revoke_tokens CPI. The default retry with priority-fee escalation
neutralises non-persistent griefing; persistent adversaries raise the victim's effective cost
but cannot deny service indefinitely.
Throughput planning
Product flows that expect multiple users to bridge the same mint concurrently should:
- Treat per-mint outbound throughput as bounded. Do not design UX around parallel sub-second completion for the same mint.
- Expect and tolerate
InvalidMessageretries in metrics — they are not bugs. - Surface retry attempts to the UI (e.g. "re-submitting due to concurrent bridge activity") rather than treating the first revert as a failure.
Related packages
@securitize/solana-bridge-cli(securitize-bridgebinary) — operational CLI built on this SDK.@securitize/solana-usdc-bridge-sdk— sibling SDK for the USDC CCTP bridge (no Wormhole sequence contention; CCTP has a different on-chain flow).
Development
This package is part of the bc-solana-bridge-sc monorepo. From the repo root:
yarn build # anchor build
yarn sync-sdk-all # copy IDL / types into SDK src
yarn build:packages