@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
package/docs/solana.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Solana for EVM developers
|
|
2
|
+
|
|
3
|
+
A focused guide for someone who already knows EVM well. We map every concept to its EVM
|
|
4
|
+
counterpart, then explain what's different.
|
|
5
|
+
|
|
6
|
+
## TL;DR mapping
|
|
7
|
+
|
|
8
|
+
| EVM concept | Solana counterpart | Note |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| Account (address) | Account (Pubkey, 32-byte ed25519) | Base58 string, no checksum |
|
|
11
|
+
| ETH balance on account | Lamports owned by the *system program* | `connection.getBalance(pubkey)` |
|
|
12
|
+
| ERC-20 token | SPL Token (mint) | Mint is a Solana address |
|
|
13
|
+
| ERC-20 balance | Associated Token Account (**ATA**) holding the balance | Wallet does NOT hold token balances directly |
|
|
14
|
+
| Transaction | Transaction (legacy or **VersionedTransaction**) | A bundle of *instructions* |
|
|
15
|
+
| Calldata | Instructions calling Programs | One tx may contain N instructions targeting different programs |
|
|
16
|
+
| `msg.sender` | Account signers list | Multiple signers per tx; first signer is the fee payer |
|
|
17
|
+
| Nonce | Recent blockhash + last valid block height | No per-account monotonic nonce — blockhash expiry replaces it |
|
|
18
|
+
| Gas units | Compute Units (CU), default 200k per ix | `ComputeBudgetProgram.setComputeUnitLimit(...)` |
|
|
19
|
+
| Gas price | Base fee (5,000 lamports per signature) + **priority fee** in microlamports/CU | Set via `ComputeBudgetProgram.setComputeUnitPrice(...)` |
|
|
20
|
+
| EIP-1559 priority fee | Same idea — `setComputeUnitPrice` is the lever | No `maxFeePerGas` cap; the price you set IS the price |
|
|
21
|
+
| Replace-by-fee (RBF) | Not supported | Once submitted, you can't bump. Just sign a higher-fee tx with the *same* blockhash within its validity window. |
|
|
22
|
+
| Mempool | Leader's local mempool | Not consensus mempool; tx flows directly to current/next leader |
|
|
23
|
+
| `eth_chainId` | Genesis block hash (CAIP-2 `solana:<first-32-of-hash>`) | Synthetic negative chainIds: mainnet `-2000`, testnet `-2001`, devnet `-2002` (matches omnichain-py `chain_ids.py`). The pre-v0 `-100/-101/-102` scheme is **NOT** registered as aliases — `networkTypeOf(-100)` throws `ChainError(ChainNotSupported)` at validation. Consumers must run `migrateLegacySolanaChainId(id)` on persisted rows *before* the value enters the SDK. |
|
|
24
|
+
| Finality | `processed → confirmed → finalized` | We use `confirmed` for status reads (~12 slots ≈ 5 s) |
|
|
25
|
+
| Block time | ~12 s | ~0.4 s |
|
|
26
|
+
|
|
27
|
+
## Accounts are everything
|
|
28
|
+
|
|
29
|
+
The single biggest difference: **all state lives in accounts**. There is no "contract storage
|
|
30
|
+
slot" model. Every piece of mutable data — token balances, NFT metadata, user preferences,
|
|
31
|
+
swap pools — is its own Solana account, owned by some program.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Account {
|
|
35
|
+
pubkey: Pubkey // base58 32-byte ed25519 public key
|
|
36
|
+
lamports: u64 // native SOL balance (in 1e-9 units)
|
|
37
|
+
owner: Pubkey // the program that controls this account's data
|
|
38
|
+
data: bytes // the program decides the layout
|
|
39
|
+
executable: bool // true for programs themselves
|
|
40
|
+
rent_epoch: u64 // legacy
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`owner` is the killer concept. A "user wallet" is owned by the **System Program**
|
|
45
|
+
(`11111111111111111111111111111111`). When the System Program owns an account, *only* it can
|
|
46
|
+
move that account's lamports. That's why `SystemProgram.transfer({fromPubkey, toPubkey,
|
|
47
|
+
lamports})` is the only way to move SOL between wallets — the System Program is the program
|
|
48
|
+
that knows how to debit `from` and credit `to`.
|
|
49
|
+
|
|
50
|
+
For SPL tokens, the **SPL Token Program** (`TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`) owns
|
|
51
|
+
the token accounts. Two flavours: classic SPL Token (above) and **Token-2022**
|
|
52
|
+
(`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`). They're separate program IDs with mostly
|
|
53
|
+
overlapping instruction layouts. **You must use the right program ID** for the mint at hand.
|
|
54
|
+
The SDK reads the mint account's `owner` field to figure out which.
|
|
55
|
+
|
|
56
|
+
## Transactions are instruction lists
|
|
57
|
+
|
|
58
|
+
A Solana transaction is `Vec<Instruction>` plus signers and a recent blockhash. Each
|
|
59
|
+
instruction is `{ program_id, accounts: Vec<Account>, data: Vec<u8> }`. The program_id field
|
|
60
|
+
routes execution to a specific program; `data` is the instruction-specific calldata; `accounts`
|
|
61
|
+
declares every account the instruction reads or writes (Solana is **explicit about side effects**
|
|
62
|
+
— if you don't list an account in `accounts`, the instruction can't touch it).
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
// EVM mental model — one call per tx:
|
|
66
|
+
contract.transfer(to, amount);
|
|
67
|
+
|
|
68
|
+
// Solana mental model — N instructions, each calling a program:
|
|
69
|
+
new TransactionBuilder([
|
|
70
|
+
ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
|
|
71
|
+
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 1000 }), // priority fee
|
|
72
|
+
createAssociatedTokenAccountInstruction(payer, ata, owner, mint, programId), // create-ATA if missing
|
|
73
|
+
createTransferCheckedInstruction(sourceAta, mint, destAta, owner, amount, decimals, [], programId),
|
|
74
|
+
]);
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Multiple instructions execute **atomically**: if any one fails (program error, account
|
|
78
|
+
constraint violation, missing signer), the whole transaction is rolled back. Solana's
|
|
79
|
+
equivalent of "revert" is `InstructionError` with a program-defined code.
|
|
80
|
+
|
|
81
|
+
### MessageV0 (VersionedTransaction)
|
|
82
|
+
|
|
83
|
+
Legacy transactions allow up to ~35 unique accounts before exceeding the 1232-byte tx limit.
|
|
84
|
+
**MessageV0** (versioned) supports Address Lookup Tables (ALTs) that pack repeated accounts
|
|
85
|
+
into 1-byte indices, raising the practical limit to hundreds. The depositron chain SDK
|
|
86
|
+
always emits MessageV0; legacy is essentially deprecated.
|
|
87
|
+
|
|
88
|
+
## Native SOL transfer
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
import { SystemProgram, VersionedTransaction, MessageV0, Connection } from '@solana/web3.js';
|
|
92
|
+
|
|
93
|
+
const connection = new Connection(rpcUrl, 'confirmed');
|
|
94
|
+
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('finalized');
|
|
95
|
+
const message = MessageV0.compile({
|
|
96
|
+
payerKey: senderPk,
|
|
97
|
+
instructions: [SystemProgram.transfer({ fromPubkey: senderPk, toPubkey: receiverPk, lamports: 1_000_000n })],
|
|
98
|
+
recentBlockhash: blockhash,
|
|
99
|
+
});
|
|
100
|
+
const tx = new VersionedTransaction(message);
|
|
101
|
+
tx.sign([senderKeypair]);
|
|
102
|
+
const signature = await connection.sendRawTransaction(tx.serialize());
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
That `signature` IS the transaction hash. Solana txs are uniquely identified by the
|
|
106
|
+
first signature (base58, 88 chars).
|
|
107
|
+
|
|
108
|
+
## SPL token transfer — the ATA gotcha
|
|
109
|
+
|
|
110
|
+
A wallet `Owner` doesn't hold token X directly. Instead, the wallet has one **Associated
|
|
111
|
+
Token Account** (ATA) per (mint, program_id) pair. The ATA address is a deterministic PDA
|
|
112
|
+
(Program-Derived Address) of `(owner, mint, token_program_id)`. You always have to:
|
|
113
|
+
|
|
114
|
+
1. Derive `source_ata = ATA(sender, mint, program)`.
|
|
115
|
+
2. Derive `dest_ata = ATA(recipient, mint, program)`.
|
|
116
|
+
3. If `dest_ata` doesn't exist on chain, **prepend** a `createAssociatedTokenAccount` ix to
|
|
117
|
+
the tx (the sender pays ~0.00203 SOL rent-exempt deposit, locked in the ATA forever).
|
|
118
|
+
4. Add `transferChecked(source_ata, mint, dest_ata, sender, amount, decimals, programId)`.
|
|
119
|
+
|
|
120
|
+
`transferChecked` is preferred over `transfer` because it includes the mint and decimals in
|
|
121
|
+
the instruction — the program verifies them, defeating "wrong-decimals" UI attacks. Use
|
|
122
|
+
`transferChecked` everywhere.
|
|
123
|
+
|
|
124
|
+
## Priority fees
|
|
125
|
+
|
|
126
|
+
There is no `maxPriorityFeePerGas` / `maxFeePerGas` split. Two knobs:
|
|
127
|
+
|
|
128
|
+
- `setComputeUnitLimit(units)` — your declared budget. Charged for whichever is smaller:
|
|
129
|
+
actual CU used or this limit. Default cap is 200k per ix, max 1.4M per tx.
|
|
130
|
+
- `setComputeUnitPrice(microLamports)` — price per CU. **This is the priority fee.** A value
|
|
131
|
+
of 1000 microlamports/CU at a 200k limit costs `0.0002 SOL = 200k × 1000 / 1e9`.
|
|
132
|
+
|
|
133
|
+
Depositron maps its `Priority` enum:
|
|
134
|
+
|
|
135
|
+
| Priority | microLamports per CU |
|
|
136
|
+
|---|---|
|
|
137
|
+
| `SLOW` | 0 |
|
|
138
|
+
| `NORMAL` | 100 |
|
|
139
|
+
| `FAST` | 1000 |
|
|
140
|
+
|
|
141
|
+
## No nonce — blockhash expiry instead
|
|
142
|
+
|
|
143
|
+
You include a **recent blockhash** in every transaction. The validator accepts the tx only if
|
|
144
|
+
the blockhash is still within ~150 slots (~1 minute) of the current tip. After that, the tx is
|
|
145
|
+
permanently invalid; you can't re-submit. To "retry" you rebuild with a fresh blockhash.
|
|
146
|
+
|
|
147
|
+
This is depositron's "PSBT-replay-impossible" property for free — once a Solana tx expires, it
|
|
148
|
+
can't be revived. The trade-off: you can't pre-sign + delay submission for >1 minute.
|
|
149
|
+
|
|
150
|
+
## Status polling
|
|
151
|
+
|
|
152
|
+
`getSignatureStatus(sig)` returns `{ confirmationStatus: 'processed'|'confirmed'|'finalized',
|
|
153
|
+
err: null|object }`. Depositron treats `confirmed | finalized` as terminal success
|
|
154
|
+
(or terminal failure if `err` is set). `processed` means "leader saw it" but not yet voted on
|
|
155
|
+
by the cluster — we keep polling.
|
|
156
|
+
|
|
157
|
+
`getTransaction(sig, { commitment: 'confirmed', maxSupportedTransactionVersion: 0 })` returns
|
|
158
|
+
the full receipt: slot, meta.fee (in lamports), meta.preBalances / postBalances (per account
|
|
159
|
+
SOL deltas) and meta.preTokenBalances / postTokenBalances (per ATA token deltas). The SDK
|
|
160
|
+
decodes these into a `NestedBalanceChanges` — the same nested Map shape EVM emits —
|
|
161
|
+
and returns a `SolanaTransactionStatus` (extends `TransactionStatus`) with `fees:
|
|
162
|
+
SolanaTransactionFees` alongside. Wallet keys are the raw base58 (case-sensitive).
|
|
163
|
+
|
|
164
|
+
Fallback path when `getTransaction` returns null:
|
|
165
|
+
- `getSignatureStatus` yields no value at all → `NotFound`.
|
|
166
|
+
- `getSignatureStatus` reports `finalized`/`confirmed` with `err` set →
|
|
167
|
+
`SolanaTransactionStatus.failed` with `fees: null` (settled-but-
|
|
168
|
+
unfetchable — fees can't be reconstructed from sig-status alone).
|
|
169
|
+
- `getSignatureStatus` reports `processed` or is otherwise settled
|
|
170
|
+
without `err` → `Pending` so consumers keep polling rather than
|
|
171
|
+
treating a settled deposit as NotFound.
|
|
172
|
+
- If `getTransaction` returns a body but `tx.meta === null` → `Pending`
|
|
173
|
+
as well.
|
|
174
|
+
|
|
175
|
+
Consumers should treat Solana `NotFound` as retryable, not terminal —
|
|
176
|
+
signature-status can lag full-tx availability on a heavily load-
|
|
177
|
+
balanced RPC.
|
|
178
|
+
|
|
179
|
+
The `balanceChangesExcludingFees(nativeAsset)` helper strips Solana's fee_payer debit
|
|
180
|
+
from the emitted changes for callers who want gross-of-fee movements. It validates that
|
|
181
|
+
`nativeAsset` matches this chain's native token (`chainId` match + empty `identifier`)
|
|
182
|
+
and throws `ChainError(InvalidArgument)` otherwise.
|
|
183
|
+
|
|
184
|
+
## Wallet derivation (BIP44 ed25519)
|
|
185
|
+
|
|
186
|
+
Depositron derives Solana keypairs via SLIP-0010 `m/44'/501'/{account}'/0'`. `501` is the
|
|
187
|
+
SLIP-44 coin type for SOL. **Unlike EVM, Solana uses ed25519, not secp256k1**, so we use the
|
|
188
|
+
`ed25519-hd-key` package — `ethers.HDNodeWallet` won't work (it produces secp256k1 keys).
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
import { derivePath } from 'ed25519-hd-key';
|
|
192
|
+
import { Keypair } from '@solana/web3.js';
|
|
193
|
+
import * as bip39 from 'bip39';
|
|
194
|
+
|
|
195
|
+
const seed = await bip39.mnemonicToSeed(mnemonic);
|
|
196
|
+
const { key } = derivePath(`m/44'/501'/${index}'/0'`, seed.toString('hex'));
|
|
197
|
+
const kp = Keypair.fromSeed(key); // 32-byte ed25519 seed → keypair
|
|
198
|
+
const address = kp.publicKey.toBase58();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Phantom and Solflare default to `m/44'/501'/0'/0'` for "account 0", incrementing the third
|
|
202
|
+
component for "account 1", "account 2", etc. (NOT the address_index slot — Solana wallets
|
|
203
|
+
typically expose ACCOUNT index, not address index).
|
|
204
|
+
|
|
205
|
+
## Common attacks / footguns
|
|
206
|
+
|
|
207
|
+
- **Wrong token program**: SPL Token vs Token-2022 — using the wrong program ID for a mint
|
|
208
|
+
causes `IncorrectProgramId`. Always read the mint account's `owner` to decide.
|
|
209
|
+
- **ATA already exists but wrong type**: a recipient may have a Token account that ISN'T the
|
|
210
|
+
associated one (legacy or another wallet's). `transferChecked` to a non-ATA still works as
|
|
211
|
+
long as it's owned by the recipient and the correct mint, but depositron always uses ATAs.
|
|
212
|
+
- **Dust attacks via SPL Token-2022 hooks**: Token-2022 supports "transfer hooks" — programs
|
|
213
|
+
that run on every transfer of that mint. A malicious mint can do near-arbitrary work in the
|
|
214
|
+
hook. We don't inspect mints for hooks; treat each mint as an opaque external program.
|
|
215
|
+
- **Rent**: every account on Solana pays ~0.00089 SOL per 128 bytes per epoch unless it has
|
|
216
|
+
enough lamports to be "rent-exempt". ATAs are 165 bytes → ~2039 lamports rent-exempt
|
|
217
|
+
deposit. When you create an ATA, the payer pays this deposit; it's recoverable by closing
|
|
218
|
+
the ATA later.
|
|
219
|
+
- **No revert reason strings**: Solana program errors are codes (`u32`), not strings. Programs
|
|
220
|
+
document them; depositron passes `code: 'REVERTED'` for failed status.
|
package/docs/utxo.md
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
# UTXO chains (Bitcoin / Litecoin / Dogecoin)
|
|
2
|
+
|
|
3
|
+
`UtxoChain` is the generic base for any Bitcoin-derived UTXO network. It
|
|
4
|
+
handles the whole transfer pipeline — UTXO fetch, branch-and-bound coin
|
|
5
|
+
selection, PSBT construction with `nonWitnessUtxo` on every input,
|
|
6
|
+
RBF sequencing, OP_RETURN memos.
|
|
7
|
+
|
|
8
|
+
`BtcChain extends UtxoChain` adds Bitcoin-only concerns (inscription /
|
|
9
|
+
rune / rare-sat filtering).
|
|
10
|
+
|
|
11
|
+
## Files
|
|
12
|
+
|
|
13
|
+
### Generic UTXO layer
|
|
14
|
+
|
|
15
|
+
- [utxo_chain.ts](../utxo/utxo_chain.ts) — `UtxoChain extends Chain`
|
|
16
|
+
- [utxo_network_params.ts](../utxo/utxo_network_params.ts) — `UtxoNetworkParams`, SLIP-44 coin ids, derivation purposes, dust default, RBF sequence
|
|
17
|
+
- [unsigned_utxo_transaction.ts](../utxo/unsigned_utxo_transaction.ts) — `UnsignedUtxoTransaction { psbtBase64, selectedInputs, feeSats, inputsToSign, … }`
|
|
18
|
+
- [utxo.ts](../utxo/utxo.ts) — `UnspentTransactionOutput`, `RawTransactionView`, `FeeEstimate`, `AddressBalance`
|
|
19
|
+
- [script.ts](../utxo/script.ts) — `detectScriptType`, `buildOpReturnScript`, `scriptTypeForAddress`
|
|
20
|
+
- [fee.ts](../utxo/fee.ts) — per-script-type vByte tables, fee math
|
|
21
|
+
- [coin_selection.ts](../utxo/coin_selection.ts) — branch-and-bound + accumulator fallback over effective-value
|
|
22
|
+
- [ecc.ts](../utxo/ecc.ts) — one-time `initEccLib(tinySecp256k1)` for Taproot
|
|
23
|
+
- [tools/](../utxo/tools) — 5 role interfaces (see below)
|
|
24
|
+
- [utxo_chains.ts](../utxo/utxo_chains.ts) — `utxoChainFromCoin` / `utxoChainFromSlip44` dispatchers
|
|
25
|
+
|
|
26
|
+
### Per-coin implementations
|
|
27
|
+
|
|
28
|
+
- [btc/](../utxo/btc) — Bitcoin (mainnet/testnet/signet/regtest) + asset indexes
|
|
29
|
+
- [ltc/](../utxo/ltc) — Litecoin (mainnet/testnet) via `coininfo`
|
|
30
|
+
- [doge/](../utxo/doge) — Dogecoin (mainnet/testnet) via `coininfo`
|
|
31
|
+
|
|
32
|
+
## Role-provider interfaces
|
|
33
|
+
|
|
34
|
+
`UtxoChain` doesn't talk to any one provider — it depends on five small
|
|
35
|
+
interfaces. A single concrete tool (like `BitcoinCoreTool`) can implement
|
|
36
|
+
all five, or you can wire different providers per role:
|
|
37
|
+
|
|
38
|
+
| Interface | Method | Default impls |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| [`UtxoProvider`](../utxo/tools/utxo_provider.ts) | `getUtxos`, `getAddressBalance` | `BitcoinCoreTool`, `EsploraTool` |
|
|
41
|
+
| [`UtxoRawTransactionProvider`](../utxo/tools/raw_transaction_provider.ts) | `getRawTransactionHex`, `getRawTransactionHexBatch`, `getTransaction` | `BitcoinCoreTool`, `EsploraTool` |
|
|
42
|
+
| [`UtxoFeeEstimator`](../utxo/tools/fee_estimator.ts) | `getFeeEstimate(targetBlocks)` | `BitcoinCoreTool` (estimatesmartfee), `EsploraTool` (Esplora `/fee-estimates`) |
|
|
43
|
+
| [`UtxoBroadcaster`](../utxo/tools/broadcaster.ts) | `broadcast(rawHex)` | `BitcoinCoreTool` (sendrawtransaction), `EsploraTool` (POST `/tx`) |
|
|
44
|
+
| [`UtxoChainTipProvider`](../utxo/tools/chain_tip_provider.ts) | `getChainTipHeight()` | `BitcoinCoreTool` (getblockcount), `EsploraTool` (`/blocks/tip/height`) |
|
|
45
|
+
|
|
46
|
+
Mix-and-match example:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const core = new BitcoinCoreTool({ /* … */ });
|
|
50
|
+
const esplora = new EsploraTool({
|
|
51
|
+
baseUrl: 'https://mempool.space/api',
|
|
52
|
+
params: BITCOIN_MAINNET_PARAMS,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const chain = bitcoinMainnetChain({
|
|
56
|
+
chainId: -1n,
|
|
57
|
+
utxoProvider: esplora, // Esplora handles arbitrary addresses without import
|
|
58
|
+
rawTxProvider: core, // Core's batch RPC fetches multiple parent txs in one round-trip
|
|
59
|
+
feeEstimator: esplora, // mempool.space has good fee estimates
|
|
60
|
+
broadcaster: core, // broadcast locally
|
|
61
|
+
chainTipProvider: esplora,
|
|
62
|
+
});
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Constructing a Bitcoin chain
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import {
|
|
69
|
+
bitcoinMainnetChain,
|
|
70
|
+
bitcoinTestnetChain,
|
|
71
|
+
bitcoinSignetChain,
|
|
72
|
+
bitcoinRegtestChain,
|
|
73
|
+
BitcoinCoreTool,
|
|
74
|
+
EsploraTool,
|
|
75
|
+
BITCOIN_MAINNET_PARAMS,
|
|
76
|
+
} from 'src/modules/chain';
|
|
77
|
+
|
|
78
|
+
const core = new BitcoinCoreTool({
|
|
79
|
+
baseUrl: 'http://localhost:8332',
|
|
80
|
+
user: 'rpcuser',
|
|
81
|
+
password: 'rpcpassword',
|
|
82
|
+
params: BITCOIN_MAINNET_PARAMS,
|
|
83
|
+
wallet: 'hot', // RPC routes to `/wallet/hot/`
|
|
84
|
+
importTimestamp: 'now', // descriptor import without rescan (default)
|
|
85
|
+
watchOnlyLabel: 'pluton', // label applied to imported descriptors
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const btc = bitcoinMainnetChain({
|
|
89
|
+
chainId: -1n,
|
|
90
|
+
utxoProvider: core,
|
|
91
|
+
rawTxProvider: core,
|
|
92
|
+
feeEstimator: core,
|
|
93
|
+
broadcaster: core,
|
|
94
|
+
chainTipProvider: core,
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
The chain registers itself with `NetworkType.BTC` in the global network-type
|
|
99
|
+
registry when constructed, so `addressFor(-1, 'bc1q…')` will return a
|
|
100
|
+
`BtcAddress` afterwards.
|
|
101
|
+
|
|
102
|
+
### Litecoin
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
import { litecoinMainnetChain, litecoinTestnetChain } from 'src/modules/chain';
|
|
106
|
+
|
|
107
|
+
const ltc = litecoinMainnetChain({
|
|
108
|
+
chainId: -10n,
|
|
109
|
+
utxoProvider: ltcCore,
|
|
110
|
+
rawTxProvider: ltcCore,
|
|
111
|
+
feeEstimator: ltcCore,
|
|
112
|
+
broadcaster: ltcCore,
|
|
113
|
+
chainTipProvider: ltcCore,
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
LTC supports BIP-44/49/84 (no Taproot). Network params come from
|
|
118
|
+
`coininfo.litecoin.main.toBitcoinJS()`.
|
|
119
|
+
|
|
120
|
+
### Dogecoin
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { dogecoinMainnetChain } from 'src/modules/chain';
|
|
124
|
+
|
|
125
|
+
const doge = dogecoinMainnetChain({
|
|
126
|
+
chainId: -20n,
|
|
127
|
+
// … same 5 role providers …
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
DOGE supports only BIP-44 (no segwit). Dust is `1_000_000` sats — DOGE's
|
|
132
|
+
network floor, much higher than BTC's 546.
|
|
133
|
+
|
|
134
|
+
### Generic dispatcher
|
|
135
|
+
|
|
136
|
+
If you'd rather not import the per-coin factory:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { utxoChainFromCoin } from 'src/modules/chain';
|
|
140
|
+
|
|
141
|
+
const chain = utxoChainFromCoin({
|
|
142
|
+
coin: 'bitcoin', // 'bitcoin-testnet' | 'bitcoin-signet' | 'bitcoin-regtest'
|
|
143
|
+
// 'litecoin' | 'litecoin-testnet'
|
|
144
|
+
// 'dogecoin' | 'dogecoin-testnet'
|
|
145
|
+
chainId: -1n,
|
|
146
|
+
utxoProvider, rawTxProvider, feeEstimator, broadcaster, chainTipProvider,
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`utxoChainFromSlip44(coinId, isMainnet, opts)` exists too if you've got
|
|
151
|
+
SLIP-44 coin IDs to dispatch on.
|
|
152
|
+
|
|
153
|
+
## Building a transfer
|
|
154
|
+
|
|
155
|
+
The `Chain` API works the same as EVM, with the BTC subclass accepting
|
|
156
|
+
extra options (`feeRateSatsPerVByte`, `excludeInscriptions`, etc.):
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const unsigned = await btc.createTransferUnsignedTransaction({
|
|
160
|
+
from: senderAddr,
|
|
161
|
+
to: recipientAddr,
|
|
162
|
+
tokenIdentifier: 'NATIVE', // BTC has no non-native tokens
|
|
163
|
+
amount: 50_000_000n, // 0.5 BTC in sats
|
|
164
|
+
feeRateSatsPerVByte: 10, // override; otherwise asks feeEstimator
|
|
165
|
+
rbfEnabled: true, // default true → sequence = 0xFFFFFFFD
|
|
166
|
+
memo: 'invoice-#123', // optional, emits an OP_RETURN output
|
|
167
|
+
excludeInscriptions: true, // optional, requires inscriptionIndex configured
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// unsigned is an UnsignedUtxoTransaction:
|
|
171
|
+
unsigned.psbtBase64 // base64-encoded PSBT (BIP-174)
|
|
172
|
+
unsigned.selectedInputs // UTXOs picked by coin selection
|
|
173
|
+
unsigned.feeSats // computed fee
|
|
174
|
+
unsigned.feeRateSatsPerVByte // rate used
|
|
175
|
+
unsigned.estimatedVBytes
|
|
176
|
+
unsigned.totalInputSats
|
|
177
|
+
unsigned.totalOutputSats
|
|
178
|
+
unsigned.changeAddress // null if change was absorbed into the fee
|
|
179
|
+
unsigned.inputsToSign // Record<address, inputIndex[]> for non-HD signers
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The PSBT carries `witnessUtxo` *and* `nonWitnessUtxo` on every input —
|
|
183
|
+
this is intentional. Legacy inputs need `nonWitnessUtxo` for correctness;
|
|
184
|
+
SegWit/Taproot inputs technically don't, but hardware wallets like Trezor
|
|
185
|
+
and Phantom demand it. Populating both is the safe default.
|
|
186
|
+
|
|
187
|
+
## Signing
|
|
188
|
+
|
|
189
|
+
The chain module never holds keys. You hand the PSBT to whatever signs
|
|
190
|
+
it — a user's wallet, a hardware device, an external service. For
|
|
191
|
+
testing or for protocol-controlled wallets that talk to Bitcoin Core:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
// Sign via Bitcoin Core's wallet (it has the keys for sender's address):
|
|
195
|
+
const processed = await rpc('walletprocesspsbt', [unsigned.psbtBase64]);
|
|
196
|
+
// processed.complete === true if Core signed every input
|
|
197
|
+
|
|
198
|
+
// Finalize → raw transaction hex:
|
|
199
|
+
const finalized = await rpc('finalizepsbt', [processed.psbt]);
|
|
200
|
+
// finalized.complete === true if all inputs are now scriptSig/witness-ready
|
|
201
|
+
// finalized.hex is broadcast-ready
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Broadcasting
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const result = await btc.broadcaster.broadcast(finalized.hex);
|
|
208
|
+
// result.txid: '<64-hex>'
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Address validation
|
|
212
|
+
|
|
213
|
+
```ts
|
|
214
|
+
btc.validateWalletAddress('bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'); // true
|
|
215
|
+
btc.validateWalletAddress('tb1q…'); // false on mainnet
|
|
216
|
+
btc.validateTokenIdentifier('NATIVE'); // true
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
All five address types are supported per chain: P2PKH (`1…`), P2SH
|
|
220
|
+
(`3…`), P2WPKH (`bc1q…` 42-char bech32), P2WSH (`bc1q…` 62-char bech32),
|
|
221
|
+
P2TR (`bc1p…` bech32m).
|
|
222
|
+
|
|
223
|
+
## Asset filtering (Bitcoin only)
|
|
224
|
+
|
|
225
|
+
To avoid accidentally spending UTXOs that carry inscriptions, runes, or
|
|
226
|
+
rare sats, wire one or more asset indexes into the chain:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
import { OrdiscanIndex, UnisatIndex } from 'src/modules/chain';
|
|
230
|
+
|
|
231
|
+
const ordiscan = new OrdiscanIndex({ apiKey: process.env.ORDISCAN_API_KEY! });
|
|
232
|
+
const unisat = new UnisatIndex({ apiKey: process.env.UNISAT_API_KEY! });
|
|
233
|
+
|
|
234
|
+
const btc = bitcoinMainnetChain({
|
|
235
|
+
chainId: -1n,
|
|
236
|
+
utxoProvider: core, rawTxProvider: core, feeEstimator: core,
|
|
237
|
+
broadcaster: core, chainTipProvider: core,
|
|
238
|
+
inscriptionIndex: unisat, // UniSat is inscription-only
|
|
239
|
+
runeIndex: ordiscan, // Ordiscan covers runes
|
|
240
|
+
rareSatIndex: ordiscan, // Ordiscan covers rare sats
|
|
241
|
+
});
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Then, per call, the caller opts into the filter:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
const unsigned = await btc.createTransferUnsignedTransaction({
|
|
248
|
+
from: senderAddr,
|
|
249
|
+
to: recipientAddr,
|
|
250
|
+
tokenIdentifier: 'NATIVE',
|
|
251
|
+
amount: 50_000_000n,
|
|
252
|
+
excludeInscriptions: true,
|
|
253
|
+
excludeRunes: true,
|
|
254
|
+
excludeRareSats: true,
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Each `excludeXxx: true` is validated against the configured index **before**
|
|
259
|
+
any HTTP call — explicit request without the corresponding index throws
|
|
260
|
+
fail-fast with a clear message:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
BtcChain ...: excludeRunes requested but no runeIndex was configured
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
If all three flags are off / omitted (default), the chain doesn't consult
|
|
267
|
+
any index. If all three are on, the chain queries each index once per
|
|
268
|
+
sender address (parallel) and unions the tainted outpoint set before
|
|
269
|
+
coin selection sees the UTXOs.
|
|
270
|
+
|
|
271
|
+
### Picking an asset-index provider
|
|
272
|
+
|
|
273
|
+
- **`OrdiscanIndex`** ([ordiscan.tool.ts](../utxo/btc/tools/ordiscan.tool.ts)) — implements all three asset interfaces. Three separate endpoints per address, fanned out in parallel.
|
|
274
|
+
- **`UnisatIndex`** ([unisat.tool.ts](../utxo/btc/tools/unisat.tool.ts)) — implements only `BtcInscriptionIndex`. Paginated.
|
|
275
|
+
- Both require an API key.
|
|
276
|
+
- For mainnet, prefer Ordiscan if you need rune/rare-sat coverage; for inscription-only filtering, UniSat is cheaper.
|
|
277
|
+
|
|
278
|
+
You can split per-concern: inscriptions from UniSat (cheaper), runes from
|
|
279
|
+
Ordiscan, etc. The interface system enforces that you can't accidentally
|
|
280
|
+
plug UniSat into a rune slot — `UnisatIndex` doesn't implement `BtcRuneIndex`.
|
|
281
|
+
|
|
282
|
+
## Coin selection
|
|
283
|
+
|
|
284
|
+
Branch-and-bound first (exact-match within `cost_of_change`, no change
|
|
285
|
+
output emitted) → accumulator (largest-first) fallback. Effective value
|
|
286
|
+
math: each UTXO's worth is `valueSats − inputVBytes * feeRate`, so the
|
|
287
|
+
selector doesn't pick UTXOs that cost more to spend than they're worth.
|
|
288
|
+
|
|
289
|
+
If change after fee would be under dust (546 sats by default), the
|
|
290
|
+
selector absorbs the surplus into the fee instead of emitting a sub-dust
|
|
291
|
+
change output that the network would reject.
|
|
292
|
+
|
|
293
|
+
Missing vs Bitcoin Core's full algorithm: knapsack fallback,
|
|
294
|
+
`OutputGroup` privacy (treating multi-UTXO same-address as one atomic
|
|
295
|
+
spend). See the [bitcoin-coin-selection][bcs] Python port for what a
|
|
296
|
+
faithful re-implementation looks like.
|
|
297
|
+
|
|
298
|
+
[bcs]: https://github.com/jamesob/bitcoin-coin-selection
|
|
299
|
+
|
|
300
|
+
## Bitcoin Core wallet integration
|
|
301
|
+
|
|
302
|
+
`BitcoinCoreTool` uses `listunspent` against a loaded wallet (faster than
|
|
303
|
+
`scantxoutset`, but the wallet has to "know" each address):
|
|
304
|
+
|
|
305
|
+
- On first `getUtxos(address)`, calls `getaddressinfo` to check if Core
|
|
306
|
+
already knows the address (`ismine` / `iswatchonly`).
|
|
307
|
+
- If not, runs `importdescriptors` with `timestamp: 'now'` (no rescan;
|
|
308
|
+
configurable via `importTimestamp`) and the `watchOnlyLabel` label.
|
|
309
|
+
- Caches the watched-address set in-process so subsequent calls skip
|
|
310
|
+
the check.
|
|
311
|
+
|
|
312
|
+
For `getrawtransaction` fetches (parent-tx hydration during PSBT
|
|
313
|
+
construction), the tool sends a **single array-body POST** with all
|
|
314
|
+
txids, taking advantage of Bitcoin Core's JSON-RPC batch protocol. One
|
|
315
|
+
round-trip regardless of how many parents the tx has.
|
|
316
|
+
|
|
317
|
+
## Esplora integration
|
|
318
|
+
|
|
319
|
+
`EsploraTool` works against Blockstream's Esplora API or any compatible
|
|
320
|
+
implementation (mempool.space is a fork). No wallet, no rescan, no
|
|
321
|
+
import — anyone-can-query against arbitrary addresses. Trade-off:
|
|
322
|
+
public Esplora caps `/address/{addr}/utxo` at 500 UTXOs.
|
|
323
|
+
|
|
324
|
+
## Testing
|
|
325
|
+
|
|
326
|
+
| Spec | Verifies |
|
|
327
|
+
|---|---|
|
|
328
|
+
| `btc_address.spec.ts` | Address validation across all 5 types × 4 networks |
|
|
329
|
+
| `script.spec.ts` | Script-type detection + OP_RETURN builder |
|
|
330
|
+
| `fee.spec.ts` | Per-script vByte tables, effective-value math |
|
|
331
|
+
| `coin_selection.spec.ts` | BnB + accumulator + change-absorbed-into-fee + multi-UTXO |
|
|
332
|
+
| `btc_chain.spec.ts` | Full PSBT round-trip with `FakeBtcTool` (in-memory provider) |
|
|
333
|
+
| `ordinal_filter.spec.ts` | Asset filter on/off + explicit-error semantics |
|
|
334
|
+
| `bitcoin_core_batch.spec.ts` | JSON-RPC array-body batching shape |
|
|
335
|
+
| `bitcoin_core_listunspent.spec.ts` | listunspent + auto-import flow |
|
|
336
|
+
| `unisat.spec.ts` | UniSat pagination + error handling |
|
|
337
|
+
| `test/integration/btc_regtest.e2e-spec.ts` | **Real Bitcoin Core regtest** — builds → signs → broadcasts a PSBT round-trip, including an OP_RETURN memo case and an insufficient-funds case |
|
|
338
|
+
|
|
339
|
+
### Running the regtest e2e
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
npm test -- test/integration/btc_regtest.e2e-spec.ts \
|
|
343
|
+
--config ./test/jest-e2e.json --runInBand
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
Spins up `ruimarinho/bitcoin-core:24-alpine` via testcontainers (no host
|
|
347
|
+
config needed beyond Docker being installed). First run pulls the image
|
|
348
|
+
(~50 MB); subsequent runs are ~38 s total.
|
|
349
|
+
|
|
350
|
+
## Adding another UTXO chain (e.g. Bitcoin Cash)
|
|
351
|
+
|
|
352
|
+
1. Add the coin to SLIP-44 mapping in [utxo_network_params.ts](../utxo/utxo_network_params.ts).
|
|
353
|
+
2. Create `chain/utxo/bch/`:
|
|
354
|
+
- `network_params.ts` — pull `coininfo.bitcoincash.main.toBitcoinJS()`,
|
|
355
|
+
set `supportedDerivationPurposes` (BCH typically only BIP-44),
|
|
356
|
+
define wallet-address regex.
|
|
357
|
+
- `bch_chains.ts` — factory functions taking `chainId` + role
|
|
358
|
+
providers, calling `new UtxoChain({ params: BCH_MAINNET_PARAMS, … })`.
|
|
359
|
+
3. Add `export * from './bch';` to [utxo/index.ts](../utxo/index.ts).
|
|
360
|
+
4. Extend the [utxoChainFromCoin](../utxo/utxo_chains.ts) dispatcher.
|
|
361
|
+
|
|
362
|
+
For chains where you need extra concerns (BCH doesn't have inscriptions
|
|
363
|
+
but has e.g. SLP tokens), follow the BTC pattern — extend `UtxoChain`
|
|
364
|
+
with a chain-specific subclass that adds the extra abstractions.
|
package/package.json
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@getomnichain/omnichain",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Multi-chain SDK for EVM, Solana, UTXO (BTC/LTC/DOGE/DASH/ZEC/BCH), TON, and Tron. TypeScript parity port of omnichain-py.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"omnichain",
|
|
7
|
+
"blockchain",
|
|
8
|
+
"evm",
|
|
9
|
+
"ethereum",
|
|
10
|
+
"solana",
|
|
11
|
+
"bitcoin",
|
|
12
|
+
"utxo",
|
|
13
|
+
"ton",
|
|
14
|
+
"tron",
|
|
15
|
+
"sdk",
|
|
16
|
+
"multichain",
|
|
17
|
+
"web3"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/getomnichain/omnichain#readme",
|
|
20
|
+
"bugs": "https://github.com/getomnichain/omnichain/issues",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"author": "getomnichain",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/getomnichain/omnichain.git"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"types": "./dist/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.ts",
|
|
33
|
+
"default": "./dist/index.js"
|
|
34
|
+
},
|
|
35
|
+
"./evm": {
|
|
36
|
+
"types": "./dist/evm/index.d.ts",
|
|
37
|
+
"default": "./dist/evm/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./solana": {
|
|
40
|
+
"types": "./dist/solana/index.d.ts",
|
|
41
|
+
"default": "./dist/solana/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./utxo": {
|
|
44
|
+
"types": "./dist/utxo/index.d.ts",
|
|
45
|
+
"default": "./dist/utxo/index.js"
|
|
46
|
+
},
|
|
47
|
+
"./ton": {
|
|
48
|
+
"types": "./dist/ton/index.d.ts",
|
|
49
|
+
"default": "./dist/ton/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./package.json": "./package.json"
|
|
52
|
+
},
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"docs",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE",
|
|
58
|
+
"CHANGELOG.md"
|
|
59
|
+
],
|
|
60
|
+
"scripts": {
|
|
61
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
62
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
63
|
+
"test": "node --experimental-vm-modules --no-warnings node_modules/jest/bin/jest.js",
|
|
64
|
+
"release": "bash scripts/release.sh",
|
|
65
|
+
"prepublishOnly": "npm run build"
|
|
66
|
+
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@solana/spl-token": "^0.4.14",
|
|
69
|
+
"@solana/web3.js": "^1.98.0",
|
|
70
|
+
"axios": "^1.7.0",
|
|
71
|
+
"bitcoinjs-lib": "^7.0.1",
|
|
72
|
+
"class-transformer": "^0.5.1",
|
|
73
|
+
"class-validator": "^0.15.1",
|
|
74
|
+
"coininfo": "^5.2.1",
|
|
75
|
+
"decimal.js": "^10.4.3",
|
|
76
|
+
"ethers": "^6.13.0",
|
|
77
|
+
"js-sha3": "^0.9.3",
|
|
78
|
+
"tiny-secp256k1": "^2.2.3"
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@jest/globals": "^29.7.0",
|
|
82
|
+
"@types/jest": "^29.5.12",
|
|
83
|
+
"@types/node": "^22.13.1",
|
|
84
|
+
"jest": "^29.7.0",
|
|
85
|
+
"ts-jest": "^29.2.0",
|
|
86
|
+
"typescript": "^5.7.0"
|
|
87
|
+
},
|
|
88
|
+
"engines": {
|
|
89
|
+
"node": ">=20.0.0"
|
|
90
|
+
},
|
|
91
|
+
"publishConfig": {
|
|
92
|
+
"access": "public"
|
|
93
|
+
}
|
|
94
|
+
}
|