@getomnichain/omnichain 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/dist/abstract_gas_pricing.d.ts +31 -0
- package/dist/abstract_gas_pricing.d.ts.map +1 -0
- package/dist/abstract_gas_pricing.js +22 -0
- package/dist/abstract_gas_pricing.js.map +1 -0
- package/dist/address.d.ts +10 -0
- package/dist/address.d.ts.map +1 -0
- package/dist/address.factory.d.ts +26 -0
- package/dist/address.factory.d.ts.map +1 -0
- package/dist/address.factory.js +75 -0
- package/dist/address.factory.js.map +1 -0
- package/dist/address.js +13 -0
- package/dist/address.js.map +1 -0
- package/dist/address_field.decorator.d.ts +17 -0
- package/dist/address_field.decorator.d.ts.map +1 -0
- package/dist/address_field.decorator.js +34 -0
- package/dist/address_field.decorator.js.map +1 -0
- package/dist/chain.base.d.ts +79 -0
- package/dist/chain.base.d.ts.map +1 -0
- package/dist/chain.base.js +113 -0
- package/dist/chain.base.js.map +1 -0
- package/dist/chain_ids.d.ts +122 -0
- package/dist/chain_ids.d.ts.map +1 -0
- package/dist/chain_ids.js +193 -0
- package/dist/chain_ids.js.map +1 -0
- package/dist/errors.d.ts +42 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +62 -0
- package/dist/errors.js.map +1 -0
- package/dist/evm/evm_address.d.ts +11 -0
- package/dist/evm/evm_address.d.ts.map +1 -0
- package/dist/evm/evm_address.js +56 -0
- package/dist/evm/evm_address.js.map +1 -0
- package/dist/evm/evm_chain.d.ts +123 -0
- package/dist/evm/evm_chain.d.ts.map +1 -0
- package/dist/evm/evm_chain.js +685 -0
- package/dist/evm/evm_chain.js.map +1 -0
- package/dist/evm/evm_chains.d.ts +83 -0
- package/dist/evm/evm_chains.d.ts.map +1 -0
- package/dist/evm/evm_chains.js +493 -0
- package/dist/evm/evm_chains.js.map +1 -0
- package/dist/evm/evm_gas_estimate.d.ts +45 -0
- package/dist/evm/evm_gas_estimate.d.ts.map +1 -0
- package/dist/evm/evm_gas_estimate.js +70 -0
- package/dist/evm/evm_gas_estimate.js.map +1 -0
- package/dist/evm/evm_gas_pricing.d.ts +40 -0
- package/dist/evm/evm_gas_pricing.d.ts.map +1 -0
- package/dist/evm/evm_gas_pricing.js +56 -0
- package/dist/evm/evm_gas_pricing.js.map +1 -0
- package/dist/evm/evm_token.d.ts +7 -0
- package/dist/evm/evm_token.d.ts.map +1 -0
- package/dist/evm/evm_token.js +22 -0
- package/dist/evm/evm_token.js.map +1 -0
- package/dist/evm/evm_tokens.d.ts +81 -0
- package/dist/evm/evm_tokens.d.ts.map +1 -0
- package/dist/evm/evm_tokens.js +104 -0
- package/dist/evm/evm_tokens.js.map +1 -0
- package/dist/evm/evm_transaction_status.d.ts +108 -0
- package/dist/evm/evm_transaction_status.d.ts.map +1 -0
- package/dist/evm/evm_transaction_status.js +177 -0
- package/dist/evm/evm_transaction_status.js.map +1 -0
- package/dist/evm/index.d.ts +10 -0
- package/dist/evm/index.d.ts.map +1 -0
- package/dist/evm/index.js +10 -0
- package/dist/evm/index.js.map +1 -0
- package/dist/evm/unsigned_evm_transaction.d.ts +19 -0
- package/dist/evm/unsigned_evm_transaction.d.ts.map +1 -0
- package/dist/evm/unsigned_evm_transaction.js +18 -0
- package/dist/evm/unsigned_evm_transaction.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/is_address.decorator.d.ts +3 -0
- package/dist/is_address.decorator.d.ts.map +1 -0
- package/dist/is_address.decorator.js +51 -0
- package/dist/is_address.decorator.js.map +1 -0
- package/dist/network_type.d.ts +73 -0
- package/dist/network_type.d.ts.map +1 -0
- package/dist/network_type.js +144 -0
- package/dist/network_type.js.map +1 -0
- package/dist/priority.d.ts +23 -0
- package/dist/priority.d.ts.map +1 -0
- package/dist/priority.js +23 -0
- package/dist/priority.js.map +1 -0
- package/dist/solana/index.d.ts +9 -0
- package/dist/solana/index.d.ts.map +1 -0
- package/dist/solana/index.js +9 -0
- package/dist/solana/index.js.map +1 -0
- package/dist/solana/solana_address.d.ts +20 -0
- package/dist/solana/solana_address.d.ts.map +1 -0
- package/dist/solana/solana_address.js +45 -0
- package/dist/solana/solana_address.js.map +1 -0
- package/dist/solana/solana_chain.d.ts +213 -0
- package/dist/solana/solana_chain.d.ts.map +1 -0
- package/dist/solana/solana_chain.js +704 -0
- package/dist/solana/solana_chain.js.map +1 -0
- package/dist/solana/solana_chains.d.ts +24 -0
- package/dist/solana/solana_chains.d.ts.map +1 -0
- package/dist/solana/solana_chains.js +84 -0
- package/dist/solana/solana_chains.js.map +1 -0
- package/dist/solana/solana_gas_pricing.d.ts +30 -0
- package/dist/solana/solana_gas_pricing.d.ts.map +1 -0
- package/dist/solana/solana_gas_pricing.js +26 -0
- package/dist/solana/solana_gas_pricing.js.map +1 -0
- package/dist/solana/solana_token.d.ts +19 -0
- package/dist/solana/solana_token.d.ts.map +1 -0
- package/dist/solana/solana_token.js +34 -0
- package/dist/solana/solana_token.js.map +1 -0
- package/dist/solana/solana_tokens.d.ts +22 -0
- package/dist/solana/solana_tokens.d.ts.map +1 -0
- package/dist/solana/solana_tokens.js +26 -0
- package/dist/solana/solana_tokens.js.map +1 -0
- package/dist/solana/solana_transaction_status.d.ts +59 -0
- package/dist/solana/solana_transaction_status.d.ts.map +1 -0
- package/dist/solana/solana_transaction_status.js +144 -0
- package/dist/solana/solana_transaction_status.js.map +1 -0
- package/dist/solana/unsigned_solana_transaction.d.ts +31 -0
- package/dist/solana/unsigned_solana_transaction.d.ts.map +1 -0
- package/dist/solana/unsigned_solana_transaction.js +26 -0
- package/dist/solana/unsigned_solana_transaction.js.map +1 -0
- package/dist/token.d.ts +35 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +62 -0
- package/dist/token.js.map +1 -0
- package/dist/ton/index.d.ts +2 -0
- package/dist/ton/index.d.ts.map +1 -0
- package/dist/ton/index.js +2 -0
- package/dist/ton/index.js.map +1 -0
- package/dist/ton/ton_address.d.ts +10 -0
- package/dist/ton/ton_address.d.ts.map +1 -0
- package/dist/ton/ton_address.js +53 -0
- package/dist/ton/ton_address.js.map +1 -0
- package/dist/transaction_status.d.ts +140 -0
- package/dist/transaction_status.d.ts.map +1 -0
- package/dist/transaction_status.js +271 -0
- package/dist/transaction_status.js.map +1 -0
- package/dist/unsigned_transaction.d.ts +7 -0
- package/dist/unsigned_transaction.d.ts.map +1 -0
- package/dist/unsigned_transaction.js +9 -0
- package/dist/unsigned_transaction.js.map +1 -0
- package/dist/utxo/btc/btc_address.d.ts +15 -0
- package/dist/utxo/btc/btc_address.d.ts.map +1 -0
- package/dist/utxo/btc/btc_address.js +39 -0
- package/dist/utxo/btc/btc_address.js.map +1 -0
- package/dist/utxo/btc/btc_chain.d.ts +34 -0
- package/dist/utxo/btc/btc_chain.d.ts.map +1 -0
- package/dist/utxo/btc/btc_chain.js +62 -0
- package/dist/utxo/btc/btc_chain.js.map +1 -0
- package/dist/utxo/btc/btc_chains.d.ts +25 -0
- package/dist/utxo/btc/btc_chains.d.ts.map +1 -0
- package/dist/utxo/btc/btc_chains.js +43 -0
- package/dist/utxo/btc/btc_chains.js.map +1 -0
- package/dist/utxo/btc/btc_token.d.ts +6 -0
- package/dist/utxo/btc/btc_token.d.ts.map +1 -0
- package/dist/utxo/btc/btc_token.js +11 -0
- package/dist/utxo/btc/btc_token.js.map +1 -0
- package/dist/utxo/btc/index.d.ts +14 -0
- package/dist/utxo/btc/index.d.ts.map +1 -0
- package/dist/utxo/btc/index.js +14 -0
- package/dist/utxo/btc/index.js.map +1 -0
- package/dist/utxo/btc/network_params.d.ts +50 -0
- package/dist/utxo/btc/network_params.d.ts.map +1 -0
- package/dist/utxo/btc/network_params.js +188 -0
- package/dist/utxo/btc/network_params.js.map +1 -0
- package/dist/utxo/btc/tools/asset_outpoint.d.ts +7 -0
- package/dist/utxo/btc/tools/asset_outpoint.d.ts.map +1 -0
- package/dist/utxo/btc/tools/asset_outpoint.js +12 -0
- package/dist/utxo/btc/tools/asset_outpoint.js.map +1 -0
- package/dist/utxo/btc/tools/bitcoin_core.tool.d.ts +41 -0
- package/dist/utxo/btc/tools/bitcoin_core.tool.d.ts.map +1 -0
- package/dist/utxo/btc/tools/bitcoin_core.tool.js +172 -0
- package/dist/utxo/btc/tools/bitcoin_core.tool.js.map +1 -0
- package/dist/utxo/btc/tools/esplora.tool.d.ts +27 -0
- package/dist/utxo/btc/tools/esplora.tool.d.ts.map +1 -0
- package/dist/utxo/btc/tools/esplora.tool.js +113 -0
- package/dist/utxo/btc/tools/esplora.tool.js.map +1 -0
- package/dist/utxo/btc/tools/inscription_index.d.ts +6 -0
- package/dist/utxo/btc/tools/inscription_index.d.ts.map +1 -0
- package/dist/utxo/btc/tools/inscription_index.js +2 -0
- package/dist/utxo/btc/tools/inscription_index.js.map +1 -0
- package/dist/utxo/btc/tools/ordiscan.tool.d.ts +19 -0
- package/dist/utxo/btc/tools/ordiscan.tool.d.ts.map +1 -0
- package/dist/utxo/btc/tools/ordiscan.tool.js +53 -0
- package/dist/utxo/btc/tools/ordiscan.tool.js.map +1 -0
- package/dist/utxo/btc/tools/rare_sat_index.d.ts +6 -0
- package/dist/utxo/btc/tools/rare_sat_index.d.ts.map +1 -0
- package/dist/utxo/btc/tools/rare_sat_index.js +2 -0
- package/dist/utxo/btc/tools/rare_sat_index.js.map +1 -0
- package/dist/utxo/btc/tools/rune_index.d.ts +6 -0
- package/dist/utxo/btc/tools/rune_index.d.ts.map +1 -0
- package/dist/utxo/btc/tools/rune_index.js +2 -0
- package/dist/utxo/btc/tools/rune_index.js.map +1 -0
- package/dist/utxo/btc/tools/unisat.tool.d.ts +18 -0
- package/dist/utxo/btc/tools/unisat.tool.d.ts.map +1 -0
- package/dist/utxo/btc/tools/unisat.tool.js +49 -0
- package/dist/utxo/btc/tools/unisat.tool.js.map +1 -0
- package/dist/utxo/coin_selection.d.ts +29 -0
- package/dist/utxo/coin_selection.d.ts.map +1 -0
- package/dist/utxo/coin_selection.js +156 -0
- package/dist/utxo/coin_selection.js.map +1 -0
- package/dist/utxo/doge/doge_chains.d.ts +17 -0
- package/dist/utxo/doge/doge_chains.d.ts.map +1 -0
- package/dist/utxo/doge/doge_chains.js +28 -0
- package/dist/utxo/doge/doge_chains.js.map +1 -0
- package/dist/utxo/doge/index.d.ts +3 -0
- package/dist/utxo/doge/index.d.ts.map +1 -0
- package/dist/utxo/doge/index.js +3 -0
- package/dist/utxo/doge/index.js.map +1 -0
- package/dist/utxo/doge/network_params.d.ts +4 -0
- package/dist/utxo/doge/network_params.d.ts.map +1 -0
- package/dist/utxo/doge/network_params.js +24 -0
- package/dist/utxo/doge/network_params.js.map +1 -0
- package/dist/utxo/ecc.d.ts +2 -0
- package/dist/utxo/ecc.d.ts.map +1 -0
- package/dist/utxo/ecc.js +11 -0
- package/dist/utxo/ecc.js.map +1 -0
- package/dist/utxo/fee.d.ts +12 -0
- package/dist/utxo/fee.d.ts.map +1 -0
- package/dist/utxo/fee.js +51 -0
- package/dist/utxo/fee.js.map +1 -0
- package/dist/utxo/index.d.ts +20 -0
- package/dist/utxo/index.d.ts.map +1 -0
- package/dist/utxo/index.js +20 -0
- package/dist/utxo/index.js.map +1 -0
- package/dist/utxo/ltc/index.d.ts +3 -0
- package/dist/utxo/ltc/index.d.ts.map +1 -0
- package/dist/utxo/ltc/index.js +3 -0
- package/dist/utxo/ltc/index.js.map +1 -0
- package/dist/utxo/ltc/ltc_chains.d.ts +17 -0
- package/dist/utxo/ltc/ltc_chains.d.ts.map +1 -0
- package/dist/utxo/ltc/ltc_chains.js +28 -0
- package/dist/utxo/ltc/ltc_chains.js.map +1 -0
- package/dist/utxo/ltc/network_params.d.ts +4 -0
- package/dist/utxo/ltc/network_params.d.ts.map +1 -0
- package/dist/utxo/ltc/network_params.js +27 -0
- package/dist/utxo/ltc/network_params.js.map +1 -0
- package/dist/utxo/script.d.ts +16 -0
- package/dist/utxo/script.d.ts.map +1 -0
- package/dist/utxo/script.js +52 -0
- package/dist/utxo/script.js.map +1 -0
- package/dist/utxo/tools/broadcaster.d.ts +6 -0
- package/dist/utxo/tools/broadcaster.d.ts.map +1 -0
- package/dist/utxo/tools/broadcaster.js +2 -0
- package/dist/utxo/tools/broadcaster.js.map +1 -0
- package/dist/utxo/tools/chain_tip_provider.d.ts +5 -0
- package/dist/utxo/tools/chain_tip_provider.d.ts.map +1 -0
- package/dist/utxo/tools/chain_tip_provider.js +2 -0
- package/dist/utxo/tools/chain_tip_provider.js.map +1 -0
- package/dist/utxo/tools/fee_estimator.d.ts +6 -0
- package/dist/utxo/tools/fee_estimator.d.ts.map +1 -0
- package/dist/utxo/tools/fee_estimator.js +2 -0
- package/dist/utxo/tools/fee_estimator.js.map +1 -0
- package/dist/utxo/tools/raw_transaction_provider.d.ts +8 -0
- package/dist/utxo/tools/raw_transaction_provider.d.ts.map +1 -0
- package/dist/utxo/tools/raw_transaction_provider.js +2 -0
- package/dist/utxo/tools/raw_transaction_provider.js.map +1 -0
- package/dist/utxo/tools/utxo_provider.d.ts +7 -0
- package/dist/utxo/tools/utxo_provider.d.ts.map +1 -0
- package/dist/utxo/tools/utxo_provider.js +2 -0
- package/dist/utxo/tools/utxo_provider.js.map +1 -0
- package/dist/utxo/unsigned_utxo_transaction.d.ts +32 -0
- package/dist/utxo/unsigned_utxo_transaction.d.ts.map +1 -0
- package/dist/utxo/unsigned_utxo_transaction.js +32 -0
- package/dist/utxo/unsigned_utxo_transaction.js.map +1 -0
- package/dist/utxo/utxo.d.ts +43 -0
- package/dist/utxo/utxo.d.ts.map +1 -0
- package/dist/utxo/utxo.js +2 -0
- package/dist/utxo/utxo.js.map +1 -0
- package/dist/utxo/utxo_chain.d.ts +102 -0
- package/dist/utxo/utxo_chain.d.ts.map +1 -0
- package/dist/utxo/utxo_chain.js +524 -0
- package/dist/utxo/utxo_chain.js.map +1 -0
- package/dist/utxo/utxo_chains.d.ts +29 -0
- package/dist/utxo/utxo_chains.d.ts.map +1 -0
- package/dist/utxo/utxo_chains.js +63 -0
- package/dist/utxo/utxo_chains.js.map +1 -0
- package/dist/utxo/utxo_gas_pricing.d.ts +22 -0
- package/dist/utxo/utxo_gas_pricing.d.ts.map +1 -0
- package/dist/utxo/utxo_gas_pricing.js +20 -0
- package/dist/utxo/utxo_gas_pricing.js.map +1 -0
- package/dist/utxo/utxo_network_params.d.ts +25 -0
- package/dist/utxo/utxo_network_params.d.ts.map +1 -0
- package/dist/utxo/utxo_network_params.js +15 -0
- package/dist/utxo/utxo_network_params.js.map +1 -0
- package/dist/utxo/utxo_transaction_status.d.ts +73 -0
- package/dist/utxo/utxo_transaction_status.d.ts.map +1 -0
- package/dist/utxo/utxo_transaction_status.js +89 -0
- package/dist/utxo/utxo_transaction_status.js.map +1 -0
- package/docs/PUBLISHING.md +119 -0
- package/docs/README.md +273 -0
- package/docs/evm.md +205 -0
- package/docs/solana.md +220 -0
- package/docs/utxo.md +364 -0
- package/package.json +94 -0
|
@@ -0,0 +1,704 @@
|
|
|
1
|
+
import { ComputeBudgetProgram, Connection, MessageV0, PublicKey, SystemProgram, VersionedTransaction, } from '@solana/web3.js';
|
|
2
|
+
import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID, createAssociatedTokenAccountIdempotentInstruction, createAssociatedTokenAccountInstruction, createTransferCheckedInstruction, getAccount, getAssociatedTokenAddressSync, getMint, } from '@solana/spl-token';
|
|
3
|
+
import { Chain, resolveTransferAmount } from "../chain.base.js";
|
|
4
|
+
import { ChainError, ChainErrorKinds, sanitizeCause, sanitizeMessage } from "../errors.js";
|
|
5
|
+
import { NetworkType, registerNonEvmChain } from "../network_type.js";
|
|
6
|
+
import { Priority } from "../priority.js";
|
|
7
|
+
import { AssetBalanceChange, } from "../transaction_status.js";
|
|
8
|
+
import { SolanaTransactionFees, SolanaTransactionStatus, } from "./solana_transaction_status.js";
|
|
9
|
+
import { SolanaAddress } from "./solana_address.js";
|
|
10
|
+
import { SolanaToken } from "./solana_token.js";
|
|
11
|
+
import { UnsignedSolanaTransaction } from "./unsigned_solana_transaction.js";
|
|
12
|
+
const DEFAULT_COMPUTE_UNIT_LIMIT = 200_000;
|
|
13
|
+
/** Percentile (0..1) of recent prioritization fees picked per Priority. */
|
|
14
|
+
const SOL_PRIORITY_PERCENTILE = {
|
|
15
|
+
[Priority.SLOW]: 0.25,
|
|
16
|
+
[Priority.NORMAL]: 0.5,
|
|
17
|
+
[Priority.FAST]: 0.9,
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Solana is account-based but very different from EVM (see docs/solana.md). Key choices made
|
|
21
|
+
* for depositron:
|
|
22
|
+
* - Transfers always build a `VersionedTransaction` (MessageV0) — legacy transactions are
|
|
23
|
+
* deprecated and can't address account-lookup tables for fee reduction.
|
|
24
|
+
* - The fee payer is always the sender (no relayer pattern here).
|
|
25
|
+
* - SPL transfers use `transferChecked` (which asserts the recipient mint matches the
|
|
26
|
+
* declared mint, defeating the entire class of "wrong-token-decimals" attacks).
|
|
27
|
+
* - When the recipient doesn't have an ATA for the mint, we *prepend* a create-ATA
|
|
28
|
+
* instruction paid by the sender. This costs ~2039 lamports of rent-exempt deposit.
|
|
29
|
+
* - We do NOT consult a curated registry to determine SPL vs Token-2022 —
|
|
30
|
+
* instead we read the mint account owner at build time. That's one RPC per transfer but
|
|
31
|
+
* it's authoritative.
|
|
32
|
+
*/
|
|
33
|
+
export class SolanaChain extends Chain {
|
|
34
|
+
defaultRpcUrl;
|
|
35
|
+
rpcUrl;
|
|
36
|
+
legacyRpcEnvNames;
|
|
37
|
+
explorerClusterSuffix;
|
|
38
|
+
chainAgnosticGenesisHash;
|
|
39
|
+
_nativeToken;
|
|
40
|
+
_connection = null;
|
|
41
|
+
constructor(init) {
|
|
42
|
+
super(init.chainId, init.name, NetworkType.SOLANA, init.blockTimeSeconds, init.nativeSymbol, init.explorerBaseUrl);
|
|
43
|
+
this.defaultRpcUrl = init.defaultRpcUrl;
|
|
44
|
+
this.rpcUrl = init.rpcUrl;
|
|
45
|
+
this.legacyRpcEnvNames = init.legacyRpcEnvNames ?? [];
|
|
46
|
+
this.explorerClusterSuffix = init.explorerClusterSuffix ?? '';
|
|
47
|
+
this.chainAgnosticGenesisHash = init.chainAgnosticGenesisHash;
|
|
48
|
+
this._nativeToken = SolanaToken.native(init.chainId, init.nativeSymbol, init.nativeDecimals ?? 9);
|
|
49
|
+
registerNonEvmChain(init.chainId, NetworkType.SOLANA);
|
|
50
|
+
}
|
|
51
|
+
get nativeToken() {
|
|
52
|
+
return this._nativeToken;
|
|
53
|
+
}
|
|
54
|
+
getConnection() {
|
|
55
|
+
if (this._connection)
|
|
56
|
+
return this._connection;
|
|
57
|
+
const rpcUrl = this.readRpcUrl();
|
|
58
|
+
this._connection = new Connection(rpcUrl, { commitment: 'confirmed' });
|
|
59
|
+
return this._connection;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Network-aware priority fee suggestion in microlamports per compute unit.
|
|
63
|
+
* Samples `getRecentPrioritizationFees({ lockedWritableAccounts: [] })` (cluster-wide, ~150 most recent slots) and
|
|
64
|
+
* picks a percentile per priority:
|
|
65
|
+
* - SLOW = p25
|
|
66
|
+
* - NORMAL = p50
|
|
67
|
+
* - FAST = p90
|
|
68
|
+
* Returns 0 if the cluster has been so quiet that the chosen percentile is 0 (legitimate
|
|
69
|
+
* outcome on devnet / off-peak mainnet). Caller plugs this into
|
|
70
|
+
* `ComputeBudgetProgram.setComputeUnitPrice({ microLamports })`.
|
|
71
|
+
*/
|
|
72
|
+
async suggestPriorityFeeMicroLamports(priority) {
|
|
73
|
+
const samples = await this.getConnection().getRecentPrioritizationFees({
|
|
74
|
+
lockedWritableAccounts: [],
|
|
75
|
+
});
|
|
76
|
+
const fees = samples
|
|
77
|
+
.map((s) => s.prioritizationFee)
|
|
78
|
+
.filter((f) => Number.isFinite(f) && f >= 0)
|
|
79
|
+
.sort((a, b) => a - b);
|
|
80
|
+
if (fees.length === 0)
|
|
81
|
+
return 0;
|
|
82
|
+
const percentile = SOL_PRIORITY_PERCENTILE[priority];
|
|
83
|
+
const idx = Math.min(fees.length - 1, Math.floor((fees.length - 1) * percentile));
|
|
84
|
+
return fees[idx];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Env var name for a Solana chain by chain id — mirrors Python's
|
|
88
|
+
* `SolanaChain.rpc_url_env_for_chain_id` (impl/solana/base.py:421-423)
|
|
89
|
+
* exactly, including the signed chainId. Shell operators must set via
|
|
90
|
+
* dotenv / docker / k8s since `SOLANA_-2000_RPC_URL` is not a settable
|
|
91
|
+
* name in sh/bash/zsh.
|
|
92
|
+
*/
|
|
93
|
+
static rpcUrlEnvForChainId(chainId) {
|
|
94
|
+
return `SOLANA_${chainId}_RPC_URL`;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Resolution precedence, mirrors omnichain-py/impl/solana/base.py:424-434
|
|
98
|
+
* plus a TS-side legacy fallback for consumers with pre-v0 env-var names:
|
|
99
|
+
* 1. constructor `rpcUrl`
|
|
100
|
+
* 2. env `<NAME_UPPERCASE_UNDERSCORED>_RPC_URL`
|
|
101
|
+
* 3. env `SOLANA_<chainId>_RPC_URL` (Python parity — signed)
|
|
102
|
+
* 4. env from `legacyRpcEnvNames` (if configured — e.g. mainnet checks
|
|
103
|
+
* the pre-rename `SOLANA_RPC_URL` here)
|
|
104
|
+
* 5. `defaultRpcUrl` (never throws — public cluster)
|
|
105
|
+
*/
|
|
106
|
+
readRpcUrl() {
|
|
107
|
+
if (this.rpcUrl && this.rpcUrl.trim().length > 0)
|
|
108
|
+
return this.rpcUrl.trim();
|
|
109
|
+
const env = globalThis.process?.env;
|
|
110
|
+
const candidates = [
|
|
111
|
+
this.name.replace(/ /g, '_').toUpperCase() + '_RPC_URL',
|
|
112
|
+
SolanaChain.rpcUrlEnvForChainId(this.chainId),
|
|
113
|
+
...this.legacyRpcEnvNames,
|
|
114
|
+
];
|
|
115
|
+
for (const key of candidates) {
|
|
116
|
+
const v = env?.[key];
|
|
117
|
+
if (v && v.trim().length > 0)
|
|
118
|
+
return v.trim();
|
|
119
|
+
}
|
|
120
|
+
return this.defaultRpcUrl;
|
|
121
|
+
}
|
|
122
|
+
getWalletExplorerUrl(address) {
|
|
123
|
+
return `${this.explorerBaseUrl}/account/${address}${this.explorerClusterSuffix}`;
|
|
124
|
+
}
|
|
125
|
+
getTokenExplorerUrl(tokenIdentifier) {
|
|
126
|
+
if (!tokenIdentifier) {
|
|
127
|
+
return this.explorerBaseUrl + this.explorerClusterSuffix;
|
|
128
|
+
}
|
|
129
|
+
return `${this.explorerBaseUrl}/token/${tokenIdentifier}${this.explorerClusterSuffix}`;
|
|
130
|
+
}
|
|
131
|
+
getTransactionExplorerUrl(txHash) {
|
|
132
|
+
return `${this.explorerBaseUrl}/tx/${txHash}${this.explorerClusterSuffix}`;
|
|
133
|
+
}
|
|
134
|
+
validateAddress(raw) {
|
|
135
|
+
try {
|
|
136
|
+
new SolanaAddress(raw);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
validateTokenIdentifier(raw) {
|
|
144
|
+
if (raw === undefined)
|
|
145
|
+
return true; // native SOL
|
|
146
|
+
return this.validateAddress(raw);
|
|
147
|
+
}
|
|
148
|
+
async getBalance(owner, tokenIdentifier) {
|
|
149
|
+
if (!this.validateAddress(owner)) {
|
|
150
|
+
throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid Solana address: ${owner}`, {
|
|
151
|
+
chainId: this.chainId,
|
|
152
|
+
address: owner,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const connection = this.getConnection();
|
|
156
|
+
const ownerPk = new PublicKey(owner);
|
|
157
|
+
try {
|
|
158
|
+
if (tokenIdentifier === undefined) {
|
|
159
|
+
return BigInt(await connection.getBalance(ownerPk));
|
|
160
|
+
}
|
|
161
|
+
if (!this.validateAddress(tokenIdentifier)) {
|
|
162
|
+
throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Invalid Solana mint: ${tokenIdentifier}`, { chainId: this.chainId, identifier: tokenIdentifier });
|
|
163
|
+
}
|
|
164
|
+
const mintPk = new PublicKey(tokenIdentifier);
|
|
165
|
+
const programId = await this.resolveTokenProgramId(mintPk);
|
|
166
|
+
const ata = getAssociatedTokenAddressSync(mintPk, ownerPk, false, programId);
|
|
167
|
+
try {
|
|
168
|
+
const acct = await getAccount(connection, ata, undefined, programId);
|
|
169
|
+
return acct.amount;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// No ATA → balance 0.
|
|
173
|
+
return 0n;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
if (err instanceof ChainError)
|
|
178
|
+
throw err;
|
|
179
|
+
throw new ChainError(ChainErrorKinds.RpcError, `Failed to read Solana balance for ${owner}/${tokenIdentifier ?? 'NATIVE'}: ${err.message}`, { chainId: this.chainId, address: owner, identifier: tokenIdentifier }, err);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
async createTransferUnsignedTransaction(req) {
|
|
183
|
+
if (!req.from) {
|
|
184
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, 'Solana transfers require an explicit `from` (no implicit signer)', { chainId: this.chainId });
|
|
185
|
+
}
|
|
186
|
+
if (!this.validateAddress(req.from)) {
|
|
187
|
+
throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid sender: ${req.from}`, {
|
|
188
|
+
chainId: this.chainId,
|
|
189
|
+
address: req.from,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (!this.validateAddress(req.to)) {
|
|
193
|
+
throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid recipient: ${req.to}`, {
|
|
194
|
+
chainId: this.chainId,
|
|
195
|
+
address: req.to,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (!this.validateTokenIdentifier(req.tokenIdentifier)) {
|
|
199
|
+
throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Invalid token identifier: ${req.tokenIdentifier}`, { chainId: this.chainId, identifier: req.tokenIdentifier });
|
|
200
|
+
}
|
|
201
|
+
// gasPricing not yet consumed by Solana builder (Phase 2 follow-up);
|
|
202
|
+
// reject rather than silently ignore.
|
|
203
|
+
if (req.gasPricing !== undefined) {
|
|
204
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, 'CreateTransferRequest.gasPricing is not yet consumed by SolanaChain (Phase 2 follow-up). Use `priorityFeeMicroLamportsPerCu` / `computeUnitLimit` on SolanaTransferOptions for now.', { chainId: this.chainId });
|
|
205
|
+
}
|
|
206
|
+
// isFullBalance requires a base-fee + rent-exempt reserve before
|
|
207
|
+
// it can be safely wired — sending all lamports leaves 0 for the
|
|
208
|
+
// 5000-lamport base fee (and ATA rent when feePayer === from on
|
|
209
|
+
// the SPL path). Deferred to a follow-up card.
|
|
210
|
+
if (req.isFullBalance === true) {
|
|
211
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, 'CreateTransferRequest.isFullBalance is not yet supported on SolanaChain — pass `amount` or `amountHr` explicitly.', { chainId: this.chainId });
|
|
212
|
+
}
|
|
213
|
+
// Only fetch decimals when amountHr is the input form — avoids the
|
|
214
|
+
// extra getMint RPC on every bigint-amount transfer. Native path
|
|
215
|
+
// uses SDK-declared decimals; SPL path uses resolveMintDecimals
|
|
216
|
+
// (which already throws on RPC failure — no defensive fallback).
|
|
217
|
+
const tokenDecimals = req.amountHr !== undefined
|
|
218
|
+
? req.tokenIdentifier === undefined
|
|
219
|
+
? this._nativeToken.decimals
|
|
220
|
+
: await this.resolveMintDecimals(new PublicKey(req.tokenIdentifier))
|
|
221
|
+
: 0;
|
|
222
|
+
const resolvedAmount = resolveTransferAmount(req, tokenDecimals);
|
|
223
|
+
const amountMr = resolvedAmount.kind === 'exact' ? resolvedAmount.amountMr : 0n;
|
|
224
|
+
if (amountMr <= 0n) {
|
|
225
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, `Transfer amount must be positive (got ${amountMr})`, { chainId: this.chainId });
|
|
226
|
+
}
|
|
227
|
+
if (req.feePayer !== undefined && !this.validateAddress(req.feePayer)) {
|
|
228
|
+
throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid feePayer: ${req.feePayer}`, {
|
|
229
|
+
chainId: this.chainId,
|
|
230
|
+
address: req.feePayer,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const connection = this.getConnection();
|
|
234
|
+
const fromPk = new PublicKey(req.from);
|
|
235
|
+
const toPk = new PublicKey(req.to);
|
|
236
|
+
const feePayerAddress = req.feePayer ?? req.from;
|
|
237
|
+
const feePayerPk = new PublicKey(feePayerAddress);
|
|
238
|
+
const instructions = [];
|
|
239
|
+
if (req.computeUnitLimit !== undefined || req.priorityFeeMicroLamportsPerCu !== undefined) {
|
|
240
|
+
instructions.push(ComputeBudgetProgram.setComputeUnitLimit({
|
|
241
|
+
units: req.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT,
|
|
242
|
+
}));
|
|
243
|
+
if (req.priorityFeeMicroLamportsPerCu !== undefined) {
|
|
244
|
+
instructions.push(ComputeBudgetProgram.setComputeUnitPrice({
|
|
245
|
+
microLamports: req.priorityFeeMicroLamportsPerCu,
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (req.tokenIdentifier === undefined) {
|
|
250
|
+
instructions.push(SystemProgram.transfer({
|
|
251
|
+
fromPubkey: fromPk,
|
|
252
|
+
toPubkey: toPk,
|
|
253
|
+
lamports: amountMr,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
const mintPk = new PublicKey(req.tokenIdentifier);
|
|
258
|
+
const programId = await this.resolveTokenProgramId(mintPk);
|
|
259
|
+
// If amountHr wasn't the input form, tokenDecimals is still 0 and
|
|
260
|
+
// we haven't fetched the mint yet — do it now for the transfer-
|
|
261
|
+
// checked instruction (which requires the real decimals).
|
|
262
|
+
const decimals = tokenDecimals > 0 ? tokenDecimals : await this.resolveMintDecimals(mintPk);
|
|
263
|
+
const sourceAta = getAssociatedTokenAddressSync(mintPk, fromPk, false, programId);
|
|
264
|
+
const destAta = getAssociatedTokenAddressSync(mintPk, toPk, false, programId);
|
|
265
|
+
// Create the destination ATA when missing — the fee payer pays rent.
|
|
266
|
+
const destInfo = await connection.getAccountInfo(destAta);
|
|
267
|
+
if (!destInfo) {
|
|
268
|
+
instructions.push(createAssociatedTokenAccountInstruction(feePayerPk, destAta, toPk, mintPk, programId));
|
|
269
|
+
}
|
|
270
|
+
instructions.push(createTransferCheckedInstruction(sourceAta, mintPk, destAta, fromPk, amountMr, decimals, [], programId));
|
|
271
|
+
}
|
|
272
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
|
|
273
|
+
const message = MessageV0.compile({
|
|
274
|
+
payerKey: feePayerPk,
|
|
275
|
+
instructions,
|
|
276
|
+
recentBlockhash: blockhash,
|
|
277
|
+
});
|
|
278
|
+
return new UnsignedSolanaTransaction({
|
|
279
|
+
chainId: this.chainId,
|
|
280
|
+
transaction: new VersionedTransaction(message),
|
|
281
|
+
feePayer: feePayerAddress,
|
|
282
|
+
recentBlockhash: blockhash,
|
|
283
|
+
lastValidBlockHeight,
|
|
284
|
+
instructions,
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Wraps a caller-supplied list of program instructions into a fully-formed
|
|
289
|
+
* UnsignedSolanaTransaction. Used by depositron's SOL_INSTRUCTIONS action type to support
|
|
290
|
+
* arbitrary program calls (staking, swaps, custom programs) without the SDK having to
|
|
291
|
+
* understand the instruction semantics. The SDK still owns the envelope: CU limit + price,
|
|
292
|
+
* MessageV0 compilation, fresh blockhash, VersionedTransaction wrapping.
|
|
293
|
+
*
|
|
294
|
+
* Caller is responsible for: instruction correctness, signer-account constraints (only the
|
|
295
|
+
* single key the caller will sign with — typically the vault key — can be flagged as signer),
|
|
296
|
+
* ATA creation (if any instruction touches an ATA that doesn't exist yet), and decimals
|
|
297
|
+
* validation (no automatic `transferChecked` fallback here).
|
|
298
|
+
*/
|
|
299
|
+
async createInstructionsUnsignedTransaction(req) {
|
|
300
|
+
if (!req.from || !this.validateAddress(req.from)) {
|
|
301
|
+
throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid sender: ${req.from}`, {
|
|
302
|
+
chainId: this.chainId,
|
|
303
|
+
address: req.from,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
if (req.feePayer !== undefined && !this.validateAddress(req.feePayer)) {
|
|
307
|
+
throw new ChainError(ChainErrorKinds.InvalidAddress, `Invalid feePayer: ${req.feePayer}`, {
|
|
308
|
+
chainId: this.chainId,
|
|
309
|
+
address: req.feePayer,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
if (!Array.isArray(req.instructions) || req.instructions.length === 0) {
|
|
313
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, 'SolanaChain.createInstructionsUnsignedTransaction requires at least one instruction', { chainId: this.chainId });
|
|
314
|
+
}
|
|
315
|
+
const feePayerAddress = req.feePayer ?? req.from;
|
|
316
|
+
const feePayerPk = new PublicKey(feePayerAddress);
|
|
317
|
+
const allIxs = [];
|
|
318
|
+
if (req.computeUnitLimit !== undefined || req.priorityFeeMicroLamportsPerCu !== undefined) {
|
|
319
|
+
allIxs.push(ComputeBudgetProgram.setComputeUnitLimit({
|
|
320
|
+
units: req.computeUnitLimit ?? DEFAULT_COMPUTE_UNIT_LIMIT,
|
|
321
|
+
}));
|
|
322
|
+
if (req.priorityFeeMicroLamportsPerCu !== undefined) {
|
|
323
|
+
allIxs.push(ComputeBudgetProgram.setComputeUnitPrice({
|
|
324
|
+
microLamports: req.priorityFeeMicroLamportsPerCu,
|
|
325
|
+
}));
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
for (const ix of req.instructions)
|
|
329
|
+
allIxs.push(ix);
|
|
330
|
+
const connection = this.getConnection();
|
|
331
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
|
|
332
|
+
const message = MessageV0.compile({
|
|
333
|
+
payerKey: feePayerPk,
|
|
334
|
+
instructions: allIxs,
|
|
335
|
+
recentBlockhash: blockhash,
|
|
336
|
+
});
|
|
337
|
+
return new UnsignedSolanaTransaction({
|
|
338
|
+
chainId: this.chainId,
|
|
339
|
+
transaction: new VersionedTransaction(message),
|
|
340
|
+
feePayer: feePayerAddress,
|
|
341
|
+
recentBlockhash: blockhash,
|
|
342
|
+
lastValidBlockHeight,
|
|
343
|
+
instructions: allIxs,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Returns a fresh UnsignedSolanaTransaction with the same instructions but a newly-fetched
|
|
348
|
+
* blockhash. Use when a previously-built tx couldn't broadcast inside its blockhash window
|
|
349
|
+
* (~60s on mainnet) — Solana drops txs whose blockhash has expired. Caller must re-sign:
|
|
350
|
+
* the new signed message has a different recent_blockhash byte sequence, so any existing
|
|
351
|
+
* signature is invalid.
|
|
352
|
+
*/
|
|
353
|
+
async refreshBlockhash(unsigned) {
|
|
354
|
+
const connection = this.getConnection();
|
|
355
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
|
|
356
|
+
if (blockhash === unsigned.recentBlockhash)
|
|
357
|
+
return unsigned;
|
|
358
|
+
const message = MessageV0.compile({
|
|
359
|
+
payerKey: unsigned.feePayerPubkey,
|
|
360
|
+
instructions: unsigned.instructions,
|
|
361
|
+
recentBlockhash: blockhash,
|
|
362
|
+
});
|
|
363
|
+
return new UnsignedSolanaTransaction({
|
|
364
|
+
chainId: unsigned.chainId,
|
|
365
|
+
transaction: new VersionedTransaction(message),
|
|
366
|
+
feePayer: unsigned.feePayer,
|
|
367
|
+
recentBlockhash: blockhash,
|
|
368
|
+
lastValidBlockHeight,
|
|
369
|
+
instructions: unsigned.instructions,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Simulates the tx and right-sizes its setComputeUnitLimit instruction (10% headroom).
|
|
374
|
+
* Lowers priority-fee cost because the fee = CU price × CU limit; oversized limits pay for
|
|
375
|
+
* unused compute. Returns the unsigned untouched if it has no CU-limit ix or simulation didn't
|
|
376
|
+
* report unitsConsumed. Throws ChainError(SimulationFailed) when the sim returns `err`.
|
|
377
|
+
*/
|
|
378
|
+
async estimateAndApplyCu(unsigned) {
|
|
379
|
+
const connection = this.getConnection();
|
|
380
|
+
const sim = await connection.simulateTransaction(unsigned.transaction, {
|
|
381
|
+
sigVerify: false,
|
|
382
|
+
replaceRecentBlockhash: true,
|
|
383
|
+
commitment: 'confirmed',
|
|
384
|
+
});
|
|
385
|
+
if (sim.value.err) {
|
|
386
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, `Solana simulation rejected the tx: ${JSON.stringify(sim.value.err)}`, { chainId: this.chainId });
|
|
387
|
+
}
|
|
388
|
+
const consumed = sim.value.unitsConsumed;
|
|
389
|
+
if (!consumed)
|
|
390
|
+
return unsigned;
|
|
391
|
+
const newLimit = Math.ceil(consumed * 1.1);
|
|
392
|
+
const setLimitIxIndex = unsigned.instructions.findIndex((ix) => ix.programId.equals(ComputeBudgetProgram.programId) &&
|
|
393
|
+
// setComputeUnitLimit ix discriminator is 0x02 in the first data byte.
|
|
394
|
+
ix.data.length >= 1 &&
|
|
395
|
+
ix.data[0] === 0x02);
|
|
396
|
+
if (setLimitIxIndex === -1)
|
|
397
|
+
return unsigned;
|
|
398
|
+
const newInstructions = [...unsigned.instructions];
|
|
399
|
+
newInstructions[setLimitIxIndex] = ComputeBudgetProgram.setComputeUnitLimit({
|
|
400
|
+
units: newLimit,
|
|
401
|
+
});
|
|
402
|
+
const message = MessageV0.compile({
|
|
403
|
+
payerKey: unsigned.feePayerPubkey,
|
|
404
|
+
instructions: newInstructions,
|
|
405
|
+
recentBlockhash: unsigned.recentBlockhash,
|
|
406
|
+
});
|
|
407
|
+
return new UnsignedSolanaTransaction({
|
|
408
|
+
chainId: unsigned.chainId,
|
|
409
|
+
transaction: new VersionedTransaction(message),
|
|
410
|
+
feePayer: unsigned.feePayer,
|
|
411
|
+
recentBlockhash: unsigned.recentBlockhash,
|
|
412
|
+
lastValidBlockHeight: unsigned.lastValidBlockHeight,
|
|
413
|
+
instructions: newInstructions,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/** Whether the given error came from a preflight simulation rejection (vs network/transport). */
|
|
417
|
+
static isSimulationError(err) {
|
|
418
|
+
if (err instanceof ChainError && err.kind === ChainErrorKinds.InvalidArgument)
|
|
419
|
+
return true;
|
|
420
|
+
const msg = err?.message ?? '';
|
|
421
|
+
return /simulation failed|transaction simulation failed/i.test(msg);
|
|
422
|
+
}
|
|
423
|
+
/** Whether the given error indicates the tx's blockhash has expired and a refresh is needed. */
|
|
424
|
+
static isBlockhashExpiredError(err) {
|
|
425
|
+
const msg = err?.message ?? '';
|
|
426
|
+
return /blockhash not found|expired blockhash|blockhash.*expired/i.test(msg);
|
|
427
|
+
}
|
|
428
|
+
async getTransactionStatus(txHash) {
|
|
429
|
+
const connection = this.getConnection();
|
|
430
|
+
// Best-effort read of the current rpc URL for message/cause scrubbing.
|
|
431
|
+
// readRpcUrl throws when nothing is configured; there'd be no
|
|
432
|
+
// network call in that case, but defensive null keeps the sanitizer
|
|
433
|
+
// total.
|
|
434
|
+
let scrubUrl = null;
|
|
435
|
+
try {
|
|
436
|
+
scrubUrl = this.readRpcUrl();
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
scrubUrl = null;
|
|
440
|
+
}
|
|
441
|
+
let tx;
|
|
442
|
+
try {
|
|
443
|
+
tx = await connection.getTransaction(txHash, {
|
|
444
|
+
commitment: 'confirmed',
|
|
445
|
+
maxSupportedTransactionVersion: 0,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
catch (err) {
|
|
449
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
450
|
+
throw new ChainError(ChainErrorKinds.RpcError, sanitizeMessage(`Failed to read Solana tx ${txHash}: ${rawMsg}`, scrubUrl), { chainId: this.chainId, txHash }, sanitizeCause(err, scrubUrl));
|
|
451
|
+
}
|
|
452
|
+
if (!tx) {
|
|
453
|
+
// Full tx not fetchable; fall back to signature status. Consume BOTH
|
|
454
|
+
// fields (`confirmationStatus` + `err`) — the previous fix that
|
|
455
|
+
// always returned Pending on settled-but-unfetchable let a settled-
|
|
456
|
+
// FAILED tx poll indefinitely.
|
|
457
|
+
let sig;
|
|
458
|
+
try {
|
|
459
|
+
sig = await connection.getSignatureStatus(txHash, { searchTransactionHistory: true });
|
|
460
|
+
}
|
|
461
|
+
catch (err) {
|
|
462
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
463
|
+
throw new ChainError(ChainErrorKinds.RpcError, sanitizeMessage(`Failed to read Solana signature status ${txHash}: ${rawMsg}`, scrubUrl), { chainId: this.chainId, txHash }, sanitizeCause(err, scrubUrl));
|
|
464
|
+
}
|
|
465
|
+
if (!sig || !sig.value)
|
|
466
|
+
return SolanaTransactionStatus.notFound(this.chainId);
|
|
467
|
+
const settled = sig.value.confirmationStatus === 'finalized' ||
|
|
468
|
+
sig.value.confirmationStatus === 'confirmed';
|
|
469
|
+
if (settled && sig.value.err) {
|
|
470
|
+
// Fees are not reconstructable from a sig-status only — factory
|
|
471
|
+
// now accepts fees: null for exactly this case.
|
|
472
|
+
return SolanaTransactionStatus.failed({
|
|
473
|
+
chainId: this.chainId,
|
|
474
|
+
inclusionAt: null,
|
|
475
|
+
error: { code: 'REVERTED', reason: JSON.stringify(sig.value.err) },
|
|
476
|
+
fees: null,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
// settled-Success without a fetchable body OR still propagating —
|
|
480
|
+
// keep polling. NotFound here would misreport a settled deposit.
|
|
481
|
+
return SolanaTransactionStatus.pending(this.chainId);
|
|
482
|
+
}
|
|
483
|
+
if (!tx.meta) {
|
|
484
|
+
// A returned tx without meta is a valid RPC response shape (some
|
|
485
|
+
// very old txs or unusual nodes). Report Pending so consumers keep
|
|
486
|
+
// polling rather than throwing from a status read; matches the
|
|
487
|
+
// _decodeBalanceChanges guard at the same file.
|
|
488
|
+
return SolanaTransactionStatus.pending(this.chainId);
|
|
489
|
+
}
|
|
490
|
+
const accounts = tx.transaction.message.staticAccountKeys.map((k) => k.toBase58());
|
|
491
|
+
const feePayer = accounts[0] ?? '';
|
|
492
|
+
if (feePayer.length === 0) {
|
|
493
|
+
throw new ChainError(ChainErrorKinds.TransactionDecodeFailed, `Solana tx ${txHash} has no fee_payer account`, { chainId: this.chainId, txHash });
|
|
494
|
+
}
|
|
495
|
+
const feeLamports = BigInt(tx.meta.fee);
|
|
496
|
+
const rawCu = tx.meta.computeUnitsConsumed;
|
|
497
|
+
const computeUnitsConsumed = rawCu === undefined || rawCu === null ? null : BigInt(rawCu);
|
|
498
|
+
const preLamports = BigInt(tx.meta.preBalances?.[0] ?? 0);
|
|
499
|
+
const postLamports = BigInt(tx.meta.postBalances?.[0] ?? 0);
|
|
500
|
+
const fees = new SolanaTransactionFees({
|
|
501
|
+
feePayer,
|
|
502
|
+
feeLamports,
|
|
503
|
+
computeUnitsConsumed,
|
|
504
|
+
netLamportsChangeByFeePayer: postLamports - preLamports,
|
|
505
|
+
});
|
|
506
|
+
const inclusionAt = tx.blockTime ? new Date(tx.blockTime * 1000) : null;
|
|
507
|
+
if (tx.meta.err) {
|
|
508
|
+
return SolanaTransactionStatus.failed({
|
|
509
|
+
chainId: this.chainId,
|
|
510
|
+
inclusionAt,
|
|
511
|
+
error: { code: 'REVERTED', reason: JSON.stringify(tx.meta.err) },
|
|
512
|
+
fees,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
// Wrap the decoder so a malformed uiTokenAmount.amount / pre/postBalance
|
|
516
|
+
// (BigInt(...) throws SyntaxError on non-numeric strings) surfaces as
|
|
517
|
+
// ChainError(TransactionDecodeFailed) rather than a raw SyntaxError.
|
|
518
|
+
// Mirrors evm_chain.ts:decodeBalanceChanges catch.
|
|
519
|
+
let balanceChanges;
|
|
520
|
+
try {
|
|
521
|
+
balanceChanges = this._decodeBalanceChanges(tx);
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
if (err instanceof ChainError)
|
|
525
|
+
throw err;
|
|
526
|
+
throw new ChainError(ChainErrorKinds.TransactionDecodeFailed, `Failed to decode Solana tx ${txHash}: ${err instanceof Error ? err.message : String(err)}`, { chainId: this.chainId, txHash }, err instanceof Error ? err : undefined);
|
|
527
|
+
}
|
|
528
|
+
return SolanaTransactionStatus.successful({
|
|
529
|
+
chainId: this.chainId,
|
|
530
|
+
inclusionAt,
|
|
531
|
+
balanceChanges,
|
|
532
|
+
fees,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async getChainTipHeight() {
|
|
536
|
+
return this.getConnection().getSlot('confirmed');
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Resolves the token program owner for a given mint. Token-2022 mints are owned by
|
|
540
|
+
* TOKEN_2022_PROGRAM_ID; classic SPL by TOKEN_PROGRAM_ID. Required for `transferChecked`
|
|
541
|
+
* and ATA derivation.
|
|
542
|
+
*/
|
|
543
|
+
async resolveTokenProgramId(mint) {
|
|
544
|
+
const connection = this.getConnection();
|
|
545
|
+
const info = await connection.getAccountInfo(mint);
|
|
546
|
+
if (!info) {
|
|
547
|
+
throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Mint ${mint.toBase58()} not found on chain`, { chainId: this.chainId, identifier: mint.toBase58() });
|
|
548
|
+
}
|
|
549
|
+
const owner = info.owner.toBase58();
|
|
550
|
+
if (owner === TOKEN_PROGRAM_ID.toBase58())
|
|
551
|
+
return TOKEN_PROGRAM_ID;
|
|
552
|
+
if (owner === TOKEN_2022_PROGRAM_ID.toBase58())
|
|
553
|
+
return TOKEN_2022_PROGRAM_ID;
|
|
554
|
+
throw new ChainError(ChainErrorKinds.InvalidTokenIdentifier, `Mint ${mint.toBase58()} is owned by ${owner}, not an SPL Token program`, { chainId: this.chainId, identifier: mint.toBase58() });
|
|
555
|
+
}
|
|
556
|
+
async resolveMintDecimals(mint) {
|
|
557
|
+
const programId = await this.resolveTokenProgramId(mint);
|
|
558
|
+
const mintInfo = await getMint(this.getConnection(), mint, undefined, programId);
|
|
559
|
+
return mintInfo.decimals;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Raw-instruction helper for callers that will compile the transaction downstream
|
|
563
|
+
* (e.g. depositron's SOL_INSTRUCTIONS action type) instead of using
|
|
564
|
+
* createTransferUnsignedTransaction's compiled-tx surface.
|
|
565
|
+
*
|
|
566
|
+
* Synchronous — no RPC.
|
|
567
|
+
*/
|
|
568
|
+
buildNativeTransferInstruction(from, to, lamports) {
|
|
569
|
+
if (lamports <= 0n) {
|
|
570
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, `Native transfer lamports must be positive (got ${lamports})`, { chainId: this.chainId });
|
|
571
|
+
}
|
|
572
|
+
return SystemProgram.transfer({
|
|
573
|
+
fromPubkey: from,
|
|
574
|
+
toPubkey: to,
|
|
575
|
+
lamports,
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Raw-instruction helper for SPL / Token-2022 transfers where the caller will compile
|
|
580
|
+
* the transaction downstream. Returns [createATA-idempotent?, transferChecked].
|
|
581
|
+
*
|
|
582
|
+
* Idempotent ATA (vs the probe-then-create pattern in createTransferUnsignedTransaction)
|
|
583
|
+
* is deliberate: when a downstream compiler runs the emitted instructions blindly,
|
|
584
|
+
* a probe done here can race with execution — the idempotent form is a no-op when the
|
|
585
|
+
* ATA exists and a create when it doesn't, without needing a probe RPC.
|
|
586
|
+
*
|
|
587
|
+
* `allowOwnerOffCurve: true` applies to BOTH source and destination ATA derivation —
|
|
588
|
+
* needed for PDA-owned vaults / program-owned recipients. Defaults to false to match
|
|
589
|
+
* createTransferUnsignedTransaction.
|
|
590
|
+
*/
|
|
591
|
+
async buildSplTransferInstructions(req) {
|
|
592
|
+
if (req.amount <= 0n) {
|
|
593
|
+
throw new ChainError(ChainErrorKinds.InvalidArgument, `SPL transfer amount must be positive (got ${req.amount})`, { chainId: this.chainId, identifier: req.mint.toBase58() });
|
|
594
|
+
}
|
|
595
|
+
const includeCreateAta = req.includeCreateAta ?? true;
|
|
596
|
+
const allowOwnerOffCurve = req.allowOwnerOffCurve ?? false;
|
|
597
|
+
const programId = await this.resolveTokenProgramId(req.mint);
|
|
598
|
+
const decimals = await this.resolveMintDecimals(req.mint);
|
|
599
|
+
const deriveAta = (owner, role) => {
|
|
600
|
+
try {
|
|
601
|
+
return getAssociatedTokenAddressSync(req.mint, owner, allowOwnerOffCurve, programId);
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
// spl-token's TokenOwnerOffCurveError ships with an empty message. Rethrow as
|
|
605
|
+
// ChainError so consumers see the SDK's declared error surface, and so the
|
|
606
|
+
// reason is discoverable (pass `allowOwnerOffCurve: true` for PDA-owned wallets).
|
|
607
|
+
throw new ChainError(ChainErrorKinds.InvalidAddress, `${role} owner is off-curve; pass allowOwnerOffCurve:true for PDA-owned wallets`, { chainId: this.chainId, address: owner.toBase58() });
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
const sourceAta = deriveAta(req.from, 'source');
|
|
611
|
+
const destAta = deriveAta(req.to, 'destination');
|
|
612
|
+
const instructions = [];
|
|
613
|
+
if (includeCreateAta) {
|
|
614
|
+
instructions.push(createAssociatedTokenAccountIdempotentInstruction(req.from, destAta, req.to, req.mint, programId));
|
|
615
|
+
}
|
|
616
|
+
instructions.push(createTransferCheckedInstruction(sourceAta, req.mint, destAta, req.from, req.amount, decimals, [], programId));
|
|
617
|
+
return instructions;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Decodes per-account lamport deltas + SPL pre/post token-balance diffs into
|
|
621
|
+
* a `NestedBalanceChanges` map. Marked internal via naming rather than TS
|
|
622
|
+
* `private` so chain-agnostic unit tests can exercise it directly without
|
|
623
|
+
* spinning up a Connection.
|
|
624
|
+
*/
|
|
625
|
+
_decodeBalanceChanges(tx) {
|
|
626
|
+
const result = new Map();
|
|
627
|
+
if (!tx.meta)
|
|
628
|
+
return result;
|
|
629
|
+
// For v0 messages, meta.preBalances/postBalances are indexed over
|
|
630
|
+
// staticAccountKeys ++ loadedAddresses.writable ++ loadedAddresses.readonly.
|
|
631
|
+
// Iterating only staticAccountKeys silently drops lamport deltas for
|
|
632
|
+
// any LUT-resolved account — a SOL deposit to a wallet that only
|
|
633
|
+
// appears via a lookup table produces no balanceChanges row at all.
|
|
634
|
+
// getTransaction() is called with maxSupportedTransactionVersion: 0
|
|
635
|
+
// so we always see the resolved addresses.
|
|
636
|
+
const staticKeys = tx.transaction.message.staticAccountKeys.map((k) => k.toBase58());
|
|
637
|
+
const loaded = tx.meta.loadedAddresses;
|
|
638
|
+
const loadedWritable = (loaded?.writable ?? []).map((k) => k.toBase58());
|
|
639
|
+
const loadedReadonly = (loaded?.readonly ?? []).map((k) => k.toBase58());
|
|
640
|
+
const accounts = [...staticKeys, ...loadedWritable, ...loadedReadonly];
|
|
641
|
+
const pre = tx.meta.preBalances ?? [];
|
|
642
|
+
const post = tx.meta.postBalances ?? [];
|
|
643
|
+
for (let i = 0; i < accounts.length; i++) {
|
|
644
|
+
const delta = BigInt(post[i] ?? 0) - BigInt(pre[i] ?? 0);
|
|
645
|
+
if (delta === 0n)
|
|
646
|
+
continue;
|
|
647
|
+
AssetBalanceChange.upsert(result, accounts[i], this._nativeToken, AssetBalanceChange.fromMr(delta, this._nativeToken.decimals));
|
|
648
|
+
}
|
|
649
|
+
const preTok = tx.meta.preTokenBalances ?? [];
|
|
650
|
+
const postTok = tx.meta.postTokenBalances ?? [];
|
|
651
|
+
const tokenKey = (b) => `${b.accountIndex}|${b.mint}`;
|
|
652
|
+
// Track the token account's owner (a wallet) and the mint's decimals,
|
|
653
|
+
// both surfaced verbatim by getTransaction's parsed token-balance meta.
|
|
654
|
+
const byKey = new Map();
|
|
655
|
+
for (const b of preTok) {
|
|
656
|
+
byKey.set(tokenKey(b), {
|
|
657
|
+
pre: BigInt(b.uiTokenAmount.amount),
|
|
658
|
+
post: 0n,
|
|
659
|
+
mint: b.mint,
|
|
660
|
+
owner: b.owner ?? null,
|
|
661
|
+
decimals: b.uiTokenAmount.decimals,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
for (const b of postTok) {
|
|
665
|
+
const k = tokenKey(b);
|
|
666
|
+
const cur = byKey.get(k);
|
|
667
|
+
if (cur) {
|
|
668
|
+
cur.post = BigInt(b.uiTokenAmount.amount);
|
|
669
|
+
// Prefer the post-owner if the pre entry lacked one (account
|
|
670
|
+
// freshly initialized inside the tx).
|
|
671
|
+
if (cur.owner === null && b.owner)
|
|
672
|
+
cur.owner = b.owner;
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
byKey.set(k, {
|
|
676
|
+
pre: 0n,
|
|
677
|
+
post: BigInt(b.uiTokenAmount.amount),
|
|
678
|
+
mint: b.mint,
|
|
679
|
+
owner: b.owner ?? null,
|
|
680
|
+
decimals: b.uiTokenAmount.decimals,
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
for (const { pre: p, post: q, mint, owner, decimals } of byKey.values()) {
|
|
685
|
+
const delta = q - p;
|
|
686
|
+
if (delta === 0n)
|
|
687
|
+
continue;
|
|
688
|
+
// Skip entries where the token-account owner is unknown — filling in
|
|
689
|
+
// the mint as if it were a wallet address would be actively wrong.
|
|
690
|
+
// Matches Python's behavior in impl/solana/base.py:704-706.
|
|
691
|
+
if (owner === null)
|
|
692
|
+
continue;
|
|
693
|
+
// Symbol is not surfaced by getTransaction; use an UNKNOWN placeholder
|
|
694
|
+
// built from the first four mint characters. Mirrors the EVM decoder's
|
|
695
|
+
// UNKNOWN_<hex-slice> shape (evm_chain.ts:480) so consumers can spot
|
|
696
|
+
// unresolved tokens uniformly.
|
|
697
|
+
const symbol = `UNKNOWN_${mint.slice(0, 4)}`;
|
|
698
|
+
const splToken = new SolanaToken(this.chainId, symbol, mint, decimals);
|
|
699
|
+
AssetBalanceChange.upsert(result, owner, splToken, AssetBalanceChange.fromMr(delta, decimals));
|
|
700
|
+
}
|
|
701
|
+
return result;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
//# sourceMappingURL=solana_chain.js.map
|