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
25 changes: 14 additions & 11 deletions bindings/node/__test__/index.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,18 @@ describe('@open-wallet-standard/core', () => {

it('derives addresses for all chains', () => {
const phrase = generateMnemonic(12);
for (const chain of ['evm', 'solana', 'sui', 'bitcoin', 'cosmos', 'tron', 'ton', 'filecoin', 'xrpl', 'nano', 'near']) {
for (const chain of ['evm', 'solana', 'sui', 'bitcoin', 'cosmos', 'tron', 'ton', 'spark', 'filecoin', 'stacks', 'xrpl', 'nano', 'near']) {
const addr = deriveAddress(phrase, chain);
assert.ok(addr.length > 0, `address should be non-empty for ${chain}`);
}
});

// ---- Universal wallet lifecycle ----

it('creates a universal wallet with 12 accounts', () => {
it('creates a universal wallet with 13 accounts', () => {
const wallet = createWallet('lifecycle-test', undefined, 12, vaultDir);
assert.equal(wallet.name, 'lifecycle-test');
assert.equal(wallet.accounts.length, 12);
assert.equal(wallet.accounts.length, 13);

const chainIds = wallet.accounts.map((a) => a.chainId);
assert.ok(chainIds.some((c) => c.startsWith('eip155:')));
Expand All @@ -77,6 +77,7 @@ describe('@open-wallet-standard/core', () => {
assert.ok(chainIds.some((c) => c.startsWith('ton:')));
assert.ok(chainIds.some((c) => c.startsWith('spark:')));
assert.ok(chainIds.some((c) => c.startsWith('fil:')));
assert.ok(chainIds.some((c) => c.startsWith('stacks:')));
assert.ok(chainIds.some((c) => c.startsWith('xrpl:')));
assert.ok(chainIds.some((c) => c.startsWith('nano:')));
assert.ok(chainIds.some((c) => c.startsWith('near:')));
Expand Down Expand Up @@ -113,7 +114,7 @@ describe('@open-wallet-standard/core', () => {

const wallet = importWalletMnemonic('mn-import', phrase, undefined, undefined, vaultDir);
assert.equal(wallet.name, 'mn-import');
assert.equal(wallet.accounts.length, 12);
assert.equal(wallet.accounts.length, 13);

const evmAcct = wallet.accounts.find((a) => a.chainId.startsWith('eip155:'));
assert.equal(evmAcct.address, expectedEvm);
Expand All @@ -126,12 +127,12 @@ describe('@open-wallet-standard/core', () => {

// ---- Private key import (secp256k1) ----

it('imports a secp256k1 private key with all 12 accounts', () => {
it('imports a secp256k1 private key with all 13 accounts', () => {
const privkey = '4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318';
const wallet = importWalletPrivateKey('pk-secp', privkey, undefined, vaultDir, 'evm');

assert.equal(wallet.name, 'pk-secp');
assert.equal(wallet.accounts.length, 12, 'should have all 12 chain accounts');
assert.equal(wallet.accounts.length, 13, 'should have all 13 chain accounts');

// Sign on EVM (provided key's curve)
const evmSig = signMessage('pk-secp', 'evm', 'hello', undefined, undefined, undefined, vaultDir);
Expand All @@ -151,11 +152,11 @@ describe('@open-wallet-standard/core', () => {

// ---- Private key import (ed25519) ----

it('imports an ed25519 private key with all 12 accounts', () => {
it('imports an ed25519 private key with all 13 accounts', () => {
const privkey = '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60';
const wallet = importWalletPrivateKey('pk-ed', privkey, undefined, vaultDir, 'solana');

assert.equal(wallet.accounts.length, 12);
assert.equal(wallet.accounts.length, 13);

// Sign on Solana (provided key)
const solSig = signMessage('pk-ed', 'solana', 'hello', undefined, undefined, undefined, vaultDir);
Expand Down Expand Up @@ -183,7 +184,7 @@ describe('@open-wallet-standard/core', () => {
);

assert.equal(wallet.name, 'pk-both');
assert.equal(wallet.accounts.length, 12, 'should have all 12 chain accounts');
assert.equal(wallet.accounts.length, 13, 'should have all 13 chain accounts');

// Sign on EVM (secp256k1 key)
const evmSig = signMessage('pk-both', 'evm', 'hello', undefined, undefined, undefined, vaultDir);
Expand All @@ -209,7 +210,7 @@ describe('@open-wallet-standard/core', () => {
// XRPL and Nano are excluded here because their signers explicitly do not
// support generic off-chain message signing without a defined convention.
// NEAR's V1 sign_message is raw ed25519 over the bytes (NEP-413 is a follow-up).
for (const chain of ['evm', 'solana', 'sui', 'bitcoin', 'cosmos', 'tron', 'ton', 'filecoin', 'near']) {
for (const chain of ['evm', 'solana', 'sui', 'bitcoin', 'cosmos', 'tron', 'ton', 'spark', 'filecoin', 'stacks', 'near']) {
const result = signMessage('all-chain-signer', chain, 'test', undefined, undefined, undefined, vaultDir);
assert.ok(result.signature.length > 0, `signature should be non-empty for ${chain}`);
}
Expand Down Expand Up @@ -238,14 +239,16 @@ describe('@open-wallet-standard/core', () => {
// NEAR transactions have no envelope; signer hashes via sha256 then ed25519
// signs the digest. Any non-empty bytes verify the signing pipeline.
const nearTxHex = '42'.repeat(80);
const stacksTxHex = '00'.repeat(5) + '04' + '00'.repeat(174);

const txHexByChain = {
solana: solTxHex,
nano: nanoTxHex,
near: nearTxHex,
stacks: stacksTxHex,
};

for (const chain of ['evm', 'solana', 'sui', 'bitcoin', 'cosmos', 'tron', 'ton', 'filecoin', 'xrpl', 'nano', 'near']) {
for (const chain of ['evm', 'solana', 'sui', 'bitcoin', 'cosmos', 'tron', 'ton', 'spark', 'filecoin', 'stacks', 'xrpl', 'nano', 'near']) {
const hex = txHexByChain[chain] ?? txHex;
const result = signTransaction('tx-signer', chain, hex, undefined, undefined, vaultDir);
assert.ok(result.signature.length > 0, `signature should be non-empty for ${chain}`);
Expand Down
7 changes: 4 additions & 3 deletions bindings/python/tests/test_bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def test_derive_address_ethereum():

def test_derive_address_all_supported_chains():
phrase = ows.generate_mnemonic(12)
for chain in ["evm", "solana", "sui", "bitcoin", "cosmos", "tron", "ton", "filecoin", "xrpl", "nano", "near"]:
for chain in ["evm", "solana", "sui", "bitcoin", "cosmos", "tron", "ton", "spark", "filecoin", "stacks", "xrpl", "nano", "near"]:
address = ows.derive_address(phrase, chain)
assert len(address) > 0

Expand All @@ -49,7 +49,7 @@ def test_create_and_list_wallets(vault_dir):
wallet = ows.create_wallet("test-wallet", vault_path_opt=vault_dir)
assert wallet["name"] == "test-wallet"
assert isinstance(wallet["accounts"], list)
assert len(wallet["accounts"]) == 12
assert len(wallet["accounts"]) == 13

# Verify each chain family is present
chain_ids = [a["chain_id"] for a in wallet["accounts"]]
Expand All @@ -62,6 +62,7 @@ def test_create_and_list_wallets(vault_dir):
assert any(c.startswith("ton:") for c in chain_ids)
assert any(c.startswith("spark:") for c in chain_ids)
assert any(c.startswith("fil:") for c in chain_ids)
assert any(c.startswith("stacks:") for c in chain_ids)
assert any(c.startswith("xrpl:") for c in chain_ids)
assert any(c.startswith("nano:") for c in chain_ids)
assert any(c.startswith("near:") for c in chain_ids)
Expand Down Expand Up @@ -111,7 +112,7 @@ def test_import_wallet_mnemonic(vault_dir):
"imported", phrase, vault_path_opt=vault_dir
)
assert wallet["name"] == "imported"
assert len(wallet["accounts"]) == 12
assert len(wallet["accounts"]) == 13

# EVM account should match derived address
evm_account = next(a for a in wallet["accounts"] if a["chain_id"].startswith("eip155:"))
Expand Down
6 changes: 5 additions & 1 deletion 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` |
| NEAR | ed25519 | 397 | `m/44'/397'/{index}'` | 64-char lowercase hex of pubkey (implicit account) | `near` |

## Known Networks
Expand Down Expand Up @@ -74,6 +75,7 @@ Each network has a canonical chain identifier. Endpoint discovery and transport
| XRPL | `xrpl:mainnet` |
| Spark | `spark:mainnet` |
| Filecoin | `fil:mainnet` |
| Stacks | `stacks:1` |
| NEAR | `near:mainnet` |
| NEAR (testnet) | `near:testnet` |

Expand Down Expand Up @@ -107,6 +109,7 @@ xrpl-testnet → xrpl:testnet
xrpl-devnet → xrpl:devnet
spark → spark:mainnet
filecoin → fil:mainnet
stacks → stacks:1
near → near:mainnet
near-testnet → near:testnet
```
Expand All @@ -133,6 +136,7 @@ Master Seed (512 bits via PBKDF2)
├── 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'/5757'/0'/0/0 → Stacks Account 0
└── m/44'/397'/0' → NEAR Account 0
```

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,13 +15,14 @@ pub enum ChainType {
Spark,
Filecoin,
Sui,
Stacks,
Xrpl,
Nano,
Near,
}

/// All supported chain families, used for universal wallet derivation.
pub const ALL_CHAIN_TYPES: [ChainType; 12] = [
pub const ALL_CHAIN_TYPES: [ChainType; 13] = [
ChainType::Evm,
ChainType::Solana,
ChainType::Bitcoin,
Expand All @@ -31,6 +32,7 @@ pub const ALL_CHAIN_TYPES: [ChainType; 12] = [
ChainType::Spark,
ChainType::Filecoin,
ChainType::Sui,
ChainType::Stacks,
ChainType::Xrpl,
ChainType::Nano,
ChainType::Near,
Expand Down Expand Up @@ -163,6 +165,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 @@ -290,6 +297,7 @@ impl ChainType {
ChainType::Spark => "spark",
ChainType::Filecoin => "fil",
ChainType::Sui => "sui",
ChainType::Stacks => "stacks",
ChainType::Xrpl => "xrpl",
ChainType::Nano => "nano",
ChainType::Near => "near",
Expand All @@ -308,6 +316,7 @@ impl ChainType {
ChainType::Spark => 8797555,
ChainType::Filecoin => 461,
ChainType::Sui => 784,
ChainType::Stacks => 5757,
ChainType::Xrpl => 144,
ChainType::Nano => 165,
ChainType::Near => 397,
Expand All @@ -326,6 +335,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),
"near" => Some(ChainType::Near),
Expand All @@ -346,6 +356,7 @@ impl fmt::Display for ChainType {
ChainType::Spark => "spark",
ChainType::Filecoin => "filecoin",
ChainType::Sui => "sui",
ChainType::Stacks => "stacks",
ChainType::Xrpl => "xrpl",
ChainType::Nano => "nano",
ChainType::Near => "near",
Expand All @@ -368,6 +379,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),
"near" => Ok(ChainType::Near),
Expand Down Expand Up @@ -401,6 +413,7 @@ mod tests {
(ChainType::Spark, "\"spark\""),
(ChainType::Filecoin, "\"filecoin\""),
(ChainType::Sui, "\"sui\""),
(ChainType::Stacks, "\"stacks\""),
(ChainType::Xrpl, "\"xrpl\""),
(ChainType::Nano, "\"nano\""),
(ChainType::Near, "\"near\""),
Expand All @@ -423,6 +436,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");
assert_eq!(ChainType::Near.namespace(), "near");
Expand All @@ -439,6 +453,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);
assert_eq!(ChainType::Near.default_coin_type(), 397);
Expand All @@ -458,6 +473,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("near"), Some(ChainType::Near));
Expand Down Expand Up @@ -641,7 +657,7 @@ mod tests {

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

#[test]
Expand Down
3 changes: 2 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,7 @@ 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 @@ -266,7 +267,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(), 23);
assert_eq!(config.rpc.len(), 24);
assert_eq!(config.rpc_url("eip155:1"), Some("https://eth.llamarpc.com"));
assert_eq!(
config.rpc_url("near:mainnet"),
Expand Down
Loading