Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/07-supported-chains.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type AssetId = `${ChainId}:${string}`;
// e.g. "eip155:8453:native" (ETH on Base)
```

The `native` token refers to the chain's native currency (ETH, SOL, SUI, XRP, BTC, ATOM, TRX, TON, etc.).
The `native` token refers to the chain's native currency (ETH, SOL, SUI, XRP, BTC, ATOM, TRX, TON, STX, etc.).

## Chain Families

Expand All @@ -39,6 +39,7 @@ OWS groups chains into families that share a cryptographic curve and address der
| XRPL | secp256k1 | 144 | `m/44'/144'/0'/0/{index}` | Base58Check (`r...`) | `xrpl` |
| Spark | secp256k1 | 8797555 | `m/84'/0'/0'/0/{index}` | `spark:` + compressed pubkey hex | `spark` |
| Filecoin | secp256k1 | 461 | `m/44'/461'/0'/0/{index}` | `f1` + base32(blake2b-160) | `fil` |
| Stacks | secp256k1 | 5757 | `m/44'/5757'/0'/0/{index}` | c32check (`SP...`) | `stacks` |

## Known Networks

Expand Down Expand Up @@ -73,6 +74,7 @@ Each network has a canonical chain identifier. Endpoint discovery and transport
| XRPL | `xrpl:mainnet` |
| Spark | `spark:mainnet` |
| Filecoin | `fil:mainnet` |
| Stacks | `stacks:1` |

Implementations MAY ship convenience endpoint defaults, but those defaults are deployment choices rather than OWS interoperability requirements.

Expand Down Expand Up @@ -104,6 +106,7 @@ xrpl-testnet → xrpl:testnet
xrpl-devnet → xrpl:devnet
spark → spark:mainnet
filecoin → fil:mainnet
stacks → stacks:1
```

Aliases MUST be resolved to full CAIP-2 identifiers before any processing. They MUST NOT appear in wallet files, policy files, or audit logs.
Expand All @@ -127,7 +130,8 @@ Master Seed (512 bits via PBKDF2)
├── m/44'/784'/0'/0'/0' → Sui Account 0
├── m/44'/144'/0'/0/0 → XRPL Account 0
├── m/84'/0'/0'/0/0 → Spark Account 0
└── m/44'/461'/0'/0/0 → Filecoin Account 0
├── m/44'/461'/0'/0/0 → Filecoin Account 0
└── m/44'/5757'/0'/0/0 → Stacks Account 0
```

For mnemonic-based wallets, a single mnemonic derives accounts across all supported chains. Those wallet files store the encrypted mnemonic, and the signer derives the appropriate private key using each chain's coin type and derivation path. Wallets imported from raw private keys instead store encrypted curve-key material directly.
Expand Down
20 changes: 18 additions & 2 deletions ows/crates/ows-core/src/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ pub enum ChainType {
Spark,
Filecoin,
Sui,
Stacks,
Xrpl,
Nano,
}

/// All supported chain families, used for universal wallet derivation.
pub const ALL_CHAIN_TYPES: [ChainType; 10] = [
pub const ALL_CHAIN_TYPES: [ChainType; 11] = [
ChainType::Evm,
ChainType::Solana,
ChainType::Bitcoin,
Expand All @@ -29,6 +30,7 @@ pub const ALL_CHAIN_TYPES: [ChainType; 10] = [
ChainType::Ton,
ChainType::Filecoin,
ChainType::Sui,
ChainType::Stacks,
ChainType::Xrpl,
ChainType::Nano,
];
Expand Down Expand Up @@ -160,6 +162,11 @@ pub const KNOWN_CHAINS: &[Chain] = &[
chain_type: ChainType::Sui,
chain_id: "sui:mainnet",
},
Chain {
name: "stacks",
chain_type: ChainType::Stacks,
chain_id: "stacks:1",
},
Chain {
name: "xrpl",
chain_type: ChainType::Xrpl,
Expand Down Expand Up @@ -277,6 +284,7 @@ impl ChainType {
ChainType::Spark => "spark",
ChainType::Filecoin => "fil",
ChainType::Sui => "sui",
ChainType::Stacks => "stacks",
ChainType::Xrpl => "xrpl",
ChainType::Nano => "nano",
}
Expand All @@ -294,6 +302,7 @@ impl ChainType {
ChainType::Spark => 8797555,
ChainType::Filecoin => 461,
ChainType::Sui => 784,
ChainType::Stacks => 5757,
ChainType::Xrpl => 144,
ChainType::Nano => 165,
}
Expand All @@ -311,6 +320,7 @@ impl ChainType {
"spark" => Some(ChainType::Spark),
"fil" => Some(ChainType::Filecoin),
"sui" => Some(ChainType::Sui),
"stacks" => Some(ChainType::Stacks),
"xrpl" => Some(ChainType::Xrpl),
"nano" => Some(ChainType::Nano),
_ => None,
Expand All @@ -330,6 +340,7 @@ impl fmt::Display for ChainType {
ChainType::Spark => "spark",
ChainType::Filecoin => "filecoin",
ChainType::Sui => "sui",
ChainType::Stacks => "stacks",
ChainType::Xrpl => "xrpl",
ChainType::Nano => "nano",
};
Expand All @@ -351,6 +362,7 @@ impl FromStr for ChainType {
"spark" => Ok(ChainType::Spark),
"filecoin" => Ok(ChainType::Filecoin),
"sui" => Ok(ChainType::Sui),
"stacks" => Ok(ChainType::Stacks),
"xrpl" => Ok(ChainType::Xrpl),
"nano" => Ok(ChainType::Nano),
_ => Err(format!("unknown chain type: {}", s)),
Expand Down Expand Up @@ -383,6 +395,7 @@ mod tests {
(ChainType::Spark, "\"spark\""),
(ChainType::Filecoin, "\"filecoin\""),
(ChainType::Sui, "\"sui\""),
(ChainType::Stacks, "\"stacks\""),
(ChainType::Xrpl, "\"xrpl\""),
(ChainType::Nano, "\"nano\""),
] {
Expand All @@ -404,6 +417,7 @@ mod tests {
assert_eq!(ChainType::Spark.namespace(), "spark");
assert_eq!(ChainType::Filecoin.namespace(), "fil");
assert_eq!(ChainType::Sui.namespace(), "sui");
assert_eq!(ChainType::Stacks.namespace(), "stacks");
assert_eq!(ChainType::Xrpl.namespace(), "xrpl");
assert_eq!(ChainType::Nano.namespace(), "nano");
}
Expand All @@ -419,6 +433,7 @@ mod tests {
assert_eq!(ChainType::Spark.default_coin_type(), 8797555);
assert_eq!(ChainType::Filecoin.default_coin_type(), 461);
assert_eq!(ChainType::Sui.default_coin_type(), 784);
assert_eq!(ChainType::Stacks.default_coin_type(), 5757);
assert_eq!(ChainType::Xrpl.default_coin_type(), 144);
assert_eq!(ChainType::Nano.default_coin_type(), 165);
}
Expand All @@ -437,6 +452,7 @@ mod tests {
assert_eq!(ChainType::from_namespace("spark"), Some(ChainType::Spark));
assert_eq!(ChainType::from_namespace("fil"), Some(ChainType::Filecoin));
assert_eq!(ChainType::from_namespace("sui"), Some(ChainType::Sui));
assert_eq!(ChainType::from_namespace("stacks"), Some(ChainType::Stacks));
assert_eq!(ChainType::from_namespace("xrpl"), Some(ChainType::Xrpl));
assert_eq!(ChainType::from_namespace("nano"), Some(ChainType::Nano));
assert_eq!(ChainType::from_namespace("unknown"), None);
Expand Down Expand Up @@ -619,7 +635,7 @@ mod tests {

#[test]
fn test_all_chain_types() {
assert_eq!(ALL_CHAIN_TYPES.len(), 10);
assert_eq!(ALL_CHAIN_TYPES.len(), 11);
}

#[test]
Expand Down
6 changes: 5 additions & 1 deletion ows/crates/ows-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ impl Config {
"sui:mainnet".into(),
"https://fullnode.mainnet.sui.io:443".into(),
);
rpc.insert(
"stacks:1".into(),
"https://api.hiro.so".into(),
);
rpc.insert("xrpl:mainnet".into(), "https://s1.ripple.com:51234".into());
rpc.insert(
"xrpl:testnet".into(),
Expand Down Expand Up @@ -264,7 +268,7 @@ mod tests {
fn test_load_or_default_nonexistent() {
let config = Config::load_or_default_from(std::path::Path::new("/nonexistent/config.json"));
// Should have all default RPCs
assert_eq!(config.rpc.len(), 21);
assert_eq!(config.rpc.len(), 22);
assert_eq!(config.rpc_url("eip155:1"), Some("https://eth.llamarpc.com"));
}

Expand Down
57 changes: 57 additions & 0 deletions ows/crates/ows-lib/src/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -701,6 +701,7 @@ fn broadcast(chain: ChainType, rpc_url: &str, signed_bytes: &[u8]) -> Result<Str
"broadcast not yet supported for Filecoin".into(),
)),
ChainType::Sui => broadcast_sui(rpc_url, signed_bytes),
ChainType::Stacks => broadcast_stacks(rpc_url, signed_bytes),
ChainType::Xrpl => broadcast_xrpl(rpc_url, signed_bytes),
ChainType::Nano => broadcast_nano(rpc_url, signed_bytes),
}
Expand Down Expand Up @@ -849,6 +850,62 @@ fn broadcast_sui(rpc_url: &str, signed_bytes: &[u8]) -> Result<String, OwsLibErr
crate::sui_grpc::execute_transaction(rpc_url, tx_part, sig_part)
}

fn broadcast_stacks(rpc_url: &str, signed_bytes: &[u8]) -> Result<String, OwsLibError> {
// Hiro API: POST /v2/transactions with raw binary body.
// curl needs a file for --data-binary with raw bytes.
let url = format!("{}/v2/transactions", rpc_url.trim_end_matches('/'));

let random_suffix: u64 = rand::random();
let tmp = std::env::temp_dir().join(format!("ows_stacks_tx_{random_suffix}.bin"));
std::fs::write(&tmp, signed_bytes)
.map_err(|e| OwsLibError::BroadcastFailed(format!("failed to write temp file: {e}")))?;

let file_arg = format!("@{}", tmp.display());
let result = Command::new("curl")
.args([
"-sSL",
"-X",
"POST",
"-H",
"Content-Type: application/octet-stream",
"--data-binary",
&file_arg,
&url,
])
.output();

// Always clean up the temp file, even if curl failed to launch.
let _ = std::fs::remove_file(&tmp);

let output =
result.map_err(|e| OwsLibError::BroadcastFailed(format!("failed to run curl: {e}")))?;

let resp = String::from_utf8_lossy(&output.stdout).to_string();

if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(&resp) {
if let Some(err) = parsed.get("error") {
let reason = parsed
.get("reason")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
return Err(OwsLibError::BroadcastFailed(format!(
"stacks broadcast error: {err} — {reason}"
)));
}
if let Some(txid) = parsed.as_str() {
return Ok(txid.to_string());
}
}

if !output.status.success() {
return Err(OwsLibError::BroadcastFailed(format!(
"stacks broadcast failed: {resp}"
)));
}

Ok(resp.trim().trim_matches('"').to_string())
}

fn broadcast_nano(rpc_url: &str, signed_bytes: &[u8]) -> Result<String, OwsLibError> {
const STATE_BLOCK_LEN: usize = 176;
const SIGNATURE_LEN: usize = 64;
Expand Down
3 changes: 3 additions & 0 deletions ows/crates/ows-signer/src/chains/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod filecoin;
pub mod nano;
pub mod solana;
pub mod spark;
pub mod stacks;
pub mod sui;
pub mod ton;
pub mod tron;
Expand All @@ -17,6 +18,7 @@ pub use self::filecoin::FilecoinSigner;
pub use self::nano::NanoSigner;
pub use self::solana::SolanaSigner;
pub use self::spark::SparkSigner;
pub use self::stacks::StacksSigner;
pub use self::sui::SuiSigner;
pub use self::ton::TonSigner;
pub use self::tron::TronSigner;
Expand All @@ -36,6 +38,7 @@ pub fn signer_for_chain(chain: ChainType) -> Box<dyn ChainSigner> {
ChainType::Ton => Box::new(TonSigner),
ChainType::Spark => Box::new(SparkSigner),
ChainType::Filecoin => Box::new(FilecoinSigner),
ChainType::Stacks => Box::new(StacksSigner::mainnet()),
ChainType::Sui => Box::new(SuiSigner),
ChainType::Xrpl => Box::new(XrplSigner),
ChainType::Nano => Box::new(NanoSigner),
Expand Down
Loading
Loading