Skip to content
Merged
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
4 changes: 0 additions & 4 deletions PRIVACY_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,6 @@ When you use the wallet, it communicates directly with blockchain nodes (RPC/RES

These requests contain your wallet address by necessity. The wallet connects to public infrastructure or endpoints you configure.

### MoonPay (Optional)

If you use the Deposit or Withdraw features, you will be redirected to MoonPay's service. MoonPay has its own privacy policy and data collection practices. Use of MoonPay is entirely optional and requires your explicit action.

### Chain Registries

The wallet may fetch chain metadata from public registries (such as the Cosmos Chain Registry) to display network information.
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ Cosmos networks are sourced from the [Cosmos Chain Registry](https://github.com/
- Multi-chain wallet from a single mnemonic
- Cosmos staking with validator APR display
- REStake compatibility detection
- MoonPay integration for fiat on/off ramp
- BeeZee staking pools (Offers)
- IBC token support

Expand Down Expand Up @@ -182,7 +181,7 @@ This project is built with the following open-source libraries:

### Integrations

- [MoonPay](https://www.moonpay.com/) - Fiat on/off ramp integration
- [Skip.go](https://go.skip.build/) - Skip Go API is an end-to-end interoperability platform

## License

Expand Down
2 changes: 1 addition & 1 deletion TERMS_OF_USE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ This software is a tool, not a financial service. Nothing in this software const

## Third-Party Services

The wallet may integrate with third-party services (blockchain networks, MoonPay, etc.). Your use of these services is subject to their respective terms and policies. We are not responsible for third-party services.
The wallet may integrate with third-party services (blockchain networks, go.skip.build, etc.). Your use of these services is subject to their respective terms and policies. We are not responsible for third-party services.

## Limitation of Liability

Expand Down
2 changes: 1 addition & 1 deletion docs/ADDING_COSMOS_CHAIN.md
Original file line number Diff line number Diff line change
Expand Up @@ -290,4 +290,4 @@ After adding a chain:
- **[Cosmos Chain Registry](https://github.com/cosmos/chain-registry)** - Source of truth for chain data
- [Chain Registry NPM Package](https://www.npmjs.com/package/chain-registry) - TypeScript types and utilities
- [BIP44 Coin Types](https://github.com/satoshilabs/slips/blob/master/slip-0044.md)
- [Cosmos SDK Documentation](https://docs.cosmos.network/)
- [Cosmos SDK Documentation](https://docs.cosmos.network/)
29,241 changes: 25,149 additions & 4,092 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
"@cosmjs/stargate": "^0.32.2",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@moonpay/moonpay-react": "^1.10.6",
"@noble/curves": "^2.0.1",
"@noble/hashes": "^1.3.3",
"@noble/secp256k1": "^2.0.0",
"@skip-go/widget": "^3.14.23",
"bip32": "^4.0.0",
"bip39": "^3.1.0",
"buffer": "^6.0.3",
Expand Down
47 changes: 47 additions & 0 deletions src/lib/assets/chainRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,51 @@ const assetCache: Map<string, RegistryAsset[]> = new Map();
const cacheExpiry: Map<string, number> = new Map();
const CACHE_DURATION = 1000 * 60 * 60; // 1 hour

/**
* Parse BeeZee liquidity pool token denom to get a readable name
* Format: ulp_factory/{factory_address}/u{asset1}_{asset2}
* Example: ulp_factory/bze13gzq40che93tgfm9kzmkpjamah5nj0j73pyhqk/uvdl_ubze -> LP
*/
function parseBeeZeeLPToken(denom: string): { symbol: string; name: string } | null {
if (!denom.startsWith('ulp_factory/')) {
return null;
}

const parts = denom.split('/');
if (parts.length < 3) {
return null;
}

const lastPart = parts[parts.length - 1];
if (!lastPart.startsWith('u')) {
return null;
}

// Remove 'u' prefix and split by '_'
const assetPart = lastPart.slice(1);
const assets = assetPart.split('_');

if (assets.length !== 2) {
return null;
}

// Map known denoms to symbols
const denomToSymbol: Record<string, string> = {
bze: 'BZE',
vdl: 'VDL',
ubze: 'BZE',
uvdl: 'VDL',
};

const symbol1 = denomToSymbol[assets[0]] || assets[0].toUpperCase();
const symbol2 = denomToSymbol[assets[1]] || assets[1].toUpperCase();

const symbol = `${symbol1}/${symbol2}`;
const name = `LP Shares`;

return { symbol, name };
}

/**
* Get chain name from network ID for Cosmos chains
* Uses the pre-bundled registry config which has chainName
Expand Down Expand Up @@ -621,6 +666,8 @@ const tokenColors: Record<string, string> = {
SPICE: '#FF6347',
};

export { parseBeeZeeLPToken };

export function getTokenColor(symbol: string): string {
return tokenColors[symbol] || '#718096';
}
118 changes: 103 additions & 15 deletions src/lib/cosmos/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { StargateClient, SigningStargateClient, defaultRegistryTypes } from '@cosmjs/stargate';
import { OfflineSigner, GeneratedType, Registry } from '@cosmjs/proto-signing';
import { Balance } from '@/types/wallet';
import { fetchWithFailover, withFailover } from '@/lib/networks';
import { fetchWithFailover, withFailover, FailoverStatusCallback } from '@/lib/networks';

// Simple protobuf encoder for BZE messages (no eval required)
// Protobuf wire format: tag = (field_number << 3) | wire_type
Expand All @@ -24,6 +24,59 @@ function encodeString(fieldNumber: number, value: string): number[] {
return [...encodeVarint(tag), ...encodeVarint(strBytes.length), ...strBytes];
}

function encodeVarintField(fieldNumber: number, value: number): number[] {
const tag = (fieldNumber << 3) | 0; // wire type 0 = varint
return [...encodeVarint(tag), ...encodeVarint(value)];
}

function encodeLengthDelimited(fieldNumber: number, payload: number[]): number[] {
const tag = (fieldNumber << 3) | 2;
return [...encodeVarint(tag), ...encodeVarint(payload.length), ...payload];
}

// Osmosis SwapAmountInRoute: pool_id (1), token_out_denom (2)
function encodeSwapAmountInRoute(route: { pool_id: number; token_out_denom: string }): number[] {
const parts: number[] = [];
if (route.pool_id !== undefined && route.pool_id !== null) {
parts.push(...encodeVarintField(1, route.pool_id));
}
if (route.token_out_denom) {
parts.push(...encodeString(2, route.token_out_denom));
}
return parts;
}

// cosmos.base.v1beta1.Coin: denom (1), amount (2)
function encodeCoin(coin: { denom: string; amount: string }): number[] {
const parts: number[] = [];
if (coin.denom) parts.push(...encodeString(1, coin.denom));
if (coin.amount) parts.push(...encodeString(2, coin.amount));
return parts;
}

// Osmosis MsgSwapExactAmountIn: sender (1), routes (2), token_in (3), token_out_min_amount (4)
function encodeMsgSwapExactAmountIn(message: {
sender: string;
routes: Array<{ pool_id: number; token_out_denom: string }>;
token_in: { denom: string; amount: string };
token_out_min_amount: string;
}): Uint8Array {
const bytes: number[] = [];
if (message.sender) bytes.push(...encodeString(1, message.sender));
for (const route of message.routes || []) {
const routeBytes = encodeSwapAmountInRoute(route);
bytes.push(...encodeLengthDelimited(2, routeBytes));
}
if (message.token_in) {
const coinBytes = encodeCoin(message.token_in);
bytes.push(...encodeLengthDelimited(3, coinBytes));
}
if (message.token_out_min_amount) {
bytes.push(...encodeString(4, message.token_out_min_amount));
}
return new Uint8Array(bytes);
}

// MsgJoinStaking: creator (1), reward_id (2), amount (3)
function encodeMsgJoinStaking(message: {
creator: string;
Expand Down Expand Up @@ -90,6 +143,33 @@ function createBzeRegistry(): Registry {
createMsgType(encodeMsgClaimStakingRewards, { creator: '', reward_id: '' })
);

// Osmosis poolmanager swap
const osmosisSwapDefaults = {
sender: '',
routes: [] as Array<{ pool_id: number; token_out_denom: string }>,
token_in: { denom: '', amount: '' },
token_out_min_amount: '',
};
registry.register(
'/osmosis.poolmanager.v1beta1.MsgSwapExactAmountIn',
createMsgType(
(msg) => {
const m = { ...osmosisSwapDefaults, ...msg };
const routes = (m.routes || []).map((r: { pool_id?: number; poolId?: number; token_out_denom?: string; tokenOutDenom?: string }) => ({
pool_id: r.pool_id ?? r.poolId ?? 0,
token_out_denom: r.token_out_denom ?? r.tokenOutDenom ?? '',
}));
return encodeMsgSwapExactAmountIn({
sender: m.sender,
routes,
token_in: m.token_in ?? m.tokenIn ?? { denom: '', amount: '' },
token_out_min_amount: m.token_out_min_amount ?? m.tokenOutMinAmount ?? '0',
});
},
osmosisSwapDefaults
)
);

return registry;
}

Expand Down Expand Up @@ -117,19 +197,24 @@ export class CosmosClient {
* Get a StargateClient with automatic failover across multiple RPC endpoints
*/
async getClientWithFailover(
rpcEndpoints: string[]
rpcEndpoints: string[],
onStatusUpdate?: FailoverStatusCallback
): Promise<{ client: StargateClient; endpoint: string }> {
const { result: client, endpoint } = await withFailover(rpcEndpoints, async (rpcEndpoint) => {
// Prefer a cached client for this endpoint if available
const cachedClient = this.clients.get(rpcEndpoint);
if (cachedClient) {
return cachedClient;
}
const { result: client, endpoint } = await withFailover(
rpcEndpoints,
async (rpcEndpoint) => {
// Prefer a cached client for this endpoint if available
const cachedClient = this.clients.get(rpcEndpoint);
if (cachedClient) {
return cachedClient;
}

const newClient = await StargateClient.connect(rpcEndpoint);
this.clients.set(rpcEndpoint, newClient);
return newClient;
});
const newClient = await StargateClient.connect(rpcEndpoint);
this.clients.set(rpcEndpoint, newClient);
return newClient;
},
{ onStatusUpdate }
);

// Ensure the successful client is cached
this.clients.set(endpoint, client);
Expand Down Expand Up @@ -173,7 +258,8 @@ export class CosmosClient {
async getBalance(
rpcEndpoints: string | string[],
address: string,
restEndpoints?: string | string[]
restEndpoints?: string | string[],
onStatusUpdate?: FailoverStatusCallback
): Promise<Balance[]> {
// Normalize to arrays
const rpcArray = Array.isArray(rpcEndpoints) ? rpcEndpoints : [rpcEndpoints];
Expand All @@ -187,7 +273,9 @@ export class CosmosClient {
try {
const data = await fetchWithFailover<{ balances: Array<{ denom: string; amount: string }> }>(
restArray,
`/cosmos/bank/v1beta1/balances/${address}`
`/cosmos/bank/v1beta1/balances/${address}`,
{},
{ onStatusUpdate }
);

if (data.balances && Array.isArray(data.balances)) {
Expand All @@ -202,7 +290,7 @@ export class CosmosClient {
}

// Fallback to RPC with failover if REST fails
const { client } = await this.getClientWithFailover(rpcArray);
const { client } = await this.getClientWithFailover(rpcArray, onStatusUpdate);
const balances = await client.getAllBalances(address);
console.log('Fetched balances via RPC:', balances);
return balances.map((b) => ({
Expand Down
75 changes: 75 additions & 0 deletions src/lib/cosmos/osmosis-pools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/**
* Osmosis DEX Pool Fetcher
*
* Fetches pools from Osmosis LCD and converts to the swap-router format.
* Supports GAMM (weighted) pools - uses constant product approximation for 2-asset pools.
*/

import type { LiquidityPool } from './swap-router';

export interface OsmosisGammPool {
'@type': string;
id: string;
pool_params?: {
swap_fee?: string;
exit_fee?: string;
};
pool_assets?: Array<{
token: { denom: string; amount: string };
weight?: string;
}>;
}

export interface OsmosisPoolsResponse {
pools: OsmosisGammPool[];
}

/**
* Fetch all pools from Osmosis LCD API
*/
export async function fetchOsmosisPools(restUrl: string): Promise<LiquidityPool[]> {
const rest = restUrl.replace(/\/$/, '');
const response = await fetch(`${rest}/osmosis/poolmanager/v1beta1/all-pools`);

if (!response.ok) {
throw new Error(`Failed to fetch Osmosis pools: ${response.statusText}`);
}

const data: OsmosisPoolsResponse = await response.json();
const pools: LiquidityPool[] = [];

for (const pool of data.pools || []) {
// Only handle 2-asset GAMM pools (constant product approximation)
if (!pool.pool_assets || pool.pool_assets.length !== 2) {
continue;
}

const swapFee = parseFloat(pool.pool_params?.swap_fee || '0.002');
const feePercent = swapFee * 100; // 0.002 -> 0.2%

const asset0 = pool.pool_assets[0];
const asset1 = pool.pool_assets[1];

// Ensure consistent ordering for pool id (denom order)
const [base, quote] =
asset0.token.denom < asset1.token.denom
? [asset0.token.denom, asset1.token.denom]
: [asset1.token.denom, asset0.token.denom];

const [reserveBase, reserveQuote] =
asset0.token.denom === base
? [asset0.token.amount, asset1.token.amount]
: [asset1.token.amount, asset0.token.amount];

pools.push({
id: pool.id,
base,
quote,
reserve_base: reserveBase,
reserve_quote: reserveQuote,
fee: feePercent.toString(),
});
}

return pools;
}
Loading