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
2 changes: 2 additions & 0 deletions bun.lock
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
"winston": "^3.17",
},
"peerDependencies": {
"@coral-xyz/anchor": "^0.30",
"@solana/web3.js": "^1.98",
"viem": "^2.32",
},
},
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@
"types": "./dist/evm/releases/index.d.ts",
"default": "./dist/evm/releases/index.js"
},
"./solana": {
"types": "./dist/solana/index.d.ts",
"default": "./dist/solana/index.js"
},
"./package.json": "./package.json",
"./dist/*": "./dist/*"
},
Expand Down
28 changes: 2 additions & 26 deletions src/evm/chains/queries.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,5 @@
import { sortChains } from "@src/helpers";
import { createChainQueries } from "@src/internal/factories/chains";
import type { Sablier } from "@src/types";
import _ from "lodash";
import * as chains from "./data";

export const chainsQueries = {
get: (chainId: number): Sablier.EVM.Chain | undefined => {
return _.find(chains, (c) => c.id === chainId);
},
getAll: (): Sablier.EVM.Chain[] => {
return sortChains(_.values(chains));
},
getBySlug: (slug: string): Sablier.EVM.Chain | undefined => {
return _.find(chains, (c) => c.slug === slug);
},
getMainnets: (): Sablier.EVM.Chain[] => {
return sortChains(_.filter(_.values(chains), (c) => !c.isTestnet));
},
getOrThrow: (chainId: number): Sablier.EVM.Chain => {
const chain = _.find(chains, (c) => c.id === chainId);
if (!chain) {
throw new Error(`Sablier SDK: Chain with ID ${chainId} not found`);
}
return chain;
},
getTestnets: (): Sablier.EVM.Chain[] => {
return sortChains(_.filter(_.values(chains), (c) => c.isTestnet));
},
};
export const chainsQueries = createChainQueries<Sablier.EVM.Chain>(chains);
30 changes: 3 additions & 27 deletions src/evm/types.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,6 @@
import type { Chain as ViemChain } from "viem";
import type { Shared } from "@src/shared/types";
import type * as enums from "./enums";

/**
* @see https://github.com/wevm/viem/discussions/3678
*/
type ChainBlockExplorer = {
name: string;
url: string;
apiUrl?: string | undefined;
};

type Repository = {
commit: string;
url: `https://github.com/sablier-labs/${string}`;
Expand All @@ -21,32 +12,17 @@ export namespace EVM {
export type Address = `0x${string}`;

export type AbiMap = { [contractName: string]: readonly object[] };
export type Chain = ViemChain & {
blockExplorers: {
[key: string]: ChainBlockExplorer;
default: ChainBlockExplorer;
};
/** Whether this chain is supported by the Sablier Interface at https://app.sablier.com. */
isSupportedByUI: boolean;
/** Whether this is a testnet network. */
isTestnet: boolean;
export type Chain = Shared.Chain & {
/** Whether this is a zkEVM like zkSync. */
isZK: boolean;
nativeCurrency: ViemChain["nativeCurrency"] & {
coinGeckoId: string;
};
rpc: {
rpc: Shared.Chain["rpc"] & {
/** Alchemy RPC URL generator. */
alchemy?: (apiKey: string) => string;
/** Default RPC URL. */
defaults: string[];
/** Infura RPC URL generator. */
infura?: (apiKey: string) => string;
/** RouteMesh RPC URL generator. */
routemesh?: (apiKey: string) => string;
};
/** Used in deployment files to identify the chain, e.g., arbitrum-sepolia. */
slug: string;
};

/**
Expand Down
1 change: 0 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from "./evm";
export * from "./helpers";
export * from "./sablier";
export * from "./types";
82 changes: 82 additions & 0 deletions src/internal/factories/chains.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { sortChains } from "@src/helpers";
import type { Shared } from "@src/shared/types";
import _ from "lodash";

/**
* Generic factory function to create type-safe chain query objects.
*
* @template T - Chain type extending ChainCommon constraint
* @param chains - Record of chain definitions keyed by chain identifier
* @returns Query object with type-safe methods for chain operations
*
* @example
* ```typescript
* const evmQueries = createChainQueries<Sablier.EVM.Chain>(evmChains);
* const solanaQueries = createChainQueries<Sablier.Solana.Chain>(solanaChains);
* ```
*/
export function createChainQueries<T extends Shared.Chain>(chains: Record<string, T>) {
return {
/**
* Find a chain by its numeric ID.
*
* @param chainId - The numeric chain identifier
* @returns The chain if found, undefined otherwise
*/
get: (chainId: number): T | undefined => {
return _.find(chains, (c) => c.id === chainId);
},

/**
* Get all chains sorted by name.
*
* @returns Array of all chains sorted alphabetically
*/
getAll: (): T[] => {
return sortChains(_.values(chains));
},

/**
* Find a chain by its slug identifier.
*
* @param slug - The chain slug (e.g., "ethereum", "solana-mainnet")
* @returns The chain if found, undefined otherwise
*/
getBySlug: (slug: string): T | undefined => {
return _.find(chains, (c) => c.slug === slug);
},

/**
* Get all mainnet chains sorted by name.
*
* @returns Array of mainnet chains sorted alphabetically
*/
getMainnets: (): T[] => {
return sortChains(_.filter(_.values(chains), (c) => !c.isTestnet));
},

/**
* Find a chain by its numeric ID, throwing an error if not found.
*
* @param chainId - The numeric chain identifier
* @returns The chain
* @throws Error if chain with the given ID is not found
*/
getOrThrow: (chainId: number): T => {
const chain = _.find(chains, (c) => c.id === chainId);
if (!chain) {
throw new Error(`Sablier SDK: Chain with ID ${chainId} not found`);
}
return chain;
},

/**
* Get all testnet chains sorted by name.
*
* @returns Array of testnet chains sorted alphabetically
*/
getTestnets: (): T[] => {
return sortChains(_.filter(_.values(chains), (c) => c.isTestnet));
},
};
}
8 changes: 7 additions & 1 deletion src/sablier.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @file This file exports the Sablier object, singleton that contains the queries for the
* chains, contracts, and releases.
* chains, contracts, and releases for both evm and solana compatible chains.
*
* @example
* ```typescript
Expand All @@ -27,6 +27,7 @@ import _ from "lodash";
import { chainsQueries as evmChainsQueries } from "./evm/chains/queries";
import { contractsQueries as evmContractsQueries } from "./evm/contracts/queries";
import { releasesQueries as evmReleasesQueries } from "./evm/releases/queries";
import { chainsQueries as solanaChainsQueries } from "./solana/chains/queries";

/**
* Has to be defined here to avoid circular dependencies.
Expand All @@ -53,7 +54,12 @@ const evm = {
releases: evmReleasesQueries,
};

const solana = {
chains: solanaChainsQueries,
};

export const sablier = {
...evm, // re-exporting for backward compatibility
evm,
solana,
};
37 changes: 37 additions & 0 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { Chain as ViemChain } from "viem";

/**
* @see https://github.com/wevm/viem/discussions/3678
*/
type ChainBlockExplorer = {
name: string;
url: string;
apiUrl?: string | undefined;
};

export namespace Shared {
/**
* Common properties shared by EVM and Solana chains.
* This type represents the minimal interface required for chain queries and operations.
*/
export type Chain = ViemChain & {
blockExplorers: {
[key: string]: ChainBlockExplorer;
default: ChainBlockExplorer;
};
/** Whether this chain is supported by the Sablier Interface at https://app.sablier.com. */
isSupportedByUI: boolean;
/** Whether this is a testnet network. */
isTestnet: boolean;
nativeCurrency: ViemChain["nativeCurrency"] & {
coinGeckoId: string;
};
rpc: {
/** Default RPC URL. */
defaults: string[];
[key: string]: unknown;
};
/** Used in deployment files to identify the chain, e.g., arbitrum-sepolia. */
slug: string;
};
}
84 changes: 84 additions & 0 deletions src/solana/chains/data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { Sablier } from "@src/types";

/**
* Solana does not have chain IDs. These are made-up numbers so that we can use the same type for EVM and Solana chains.
*/
const CHAIN_ID_SOLANA_MAINNET_BETA = 900000010;
const CHAIN_ID_SOLANA_DEVNET = 900000020;

export const solanaDevnet: Sablier.Solana.Chain = {
blockExplorers: {
default: { name: "Explorer", url: "https://solscan.io?cluster=devnet" },
solanaFm: {
name: "Solana FM",
url: "https://solana.fm?cluster=devnet-alpha",
},
},
chainlink: {
feed: "99B2bTijsU6f1GCT73HmdR7HCFFjGMBcPZY6jZ96ynrR",
program: "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny",
},
contracts: {},
definition: {
chainCode: "SOLDEV",
chainId: CHAIN_ID_SOLANA_DEVNET,
cluster: "devnet",
},
id: CHAIN_ID_SOLANA_DEVNET,
isSupportedByUI: true,
isTestnet: true,
name: "Devnet",
nativeCurrency: {
coinGeckoId: "solana",
decimals: 9,
name: "Solana",
symbol: "SOL",
},
rpc: {
defaults: ["https://api.devnet-beta.solana.com/"],
helius: (key) => `https://devnet.helius-rpc.com/?api-key=${key}`,
},
rpcUrls: {
default: {
http: ["https://api.devnet-beta.solana.com/"],
},
},
slug: "solana-devnet",
} as const;

export const solanaMainnetBeta: Sablier.Solana.Chain = {
blockExplorers: {
default: { name: "Explorer", url: "https://solscan.io" },
solanaFm: { name: "Solana FM", url: "https://solana.fm" },
},
chainlink: {
feed: "99B2bTijsU6f1GCT73HmdR7HCFFjGMBcPZY6jZ96ynrR",
program: "HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny",
},
contracts: {},
definition: {
chainCode: "SOL",
chainId: CHAIN_ID_SOLANA_MAINNET_BETA,
cluster: "mainnet-beta",
},
id: CHAIN_ID_SOLANA_MAINNET_BETA,
isSupportedByUI: true,
isTestnet: false,
name: "Solana",
nativeCurrency: {
coinGeckoId: "solana",
decimals: 9,
name: "Solana",
symbol: "SOL",
},
rpc: {
defaults: ["https://api.mainnet-beta.solana.com/"],
helius: (key) => `https://mainnet.helius-rpc.com/?api-key=${key}`,
},
rpcUrls: {
default: {
http: ["https://api.mainnet-beta.solana.com/"],
},
},
slug: "solana-mainnet-beta",
} as const;
2 changes: 2 additions & 0 deletions src/solana/chains/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * as chains from "./data";
export * from "./data";
5 changes: 5 additions & 0 deletions src/solana/chains/queries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createChainQueries } from "@src/internal/factories/chains";
import type { Sablier } from "@src/types";
import * as chains from "./data";

export const chainsQueries = createChainQueries<Sablier.Solana.Chain>(chains);
15 changes: 15 additions & 0 deletions src/solana/enums.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export enum ChainCode {
mainnet = "SOL",
devnet = "SOLDEV",
testnet = "SOLTEST",
}
export enum Cluster {
mainnet = "mainnet-beta",
devnet = "devnet",
testnet = "testnet",
}

export const enums = {
ChainCode,
Cluster,
};
1 change: 1 addition & 0 deletions src/solana/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { chains } from "./chains";
24 changes: 24 additions & 0 deletions src/solana/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import type { Shared } from "@src/shared/types";
import type * as enums from "./enums";

export namespace Solana {
export type Address = string;
export type ChainCode = `${enums.ChainCode}` | enums.ChainCode;
export type Cluster = `${enums.Cluster}` | enums.Cluster;

export type Chain = Shared.Chain & {
rpc: Shared.Chain["rpc"] & {
/** Helius RPC URL generator. */
helius?: (apiKey: string) => string;
};
chainlink: {
program: Address; // Chainlink program used to retrieve on-chain price feeds
feed: Address; // Account providing the SOL/USD price feed data.
};
definition: {
chainCode: ChainCode;
chainId: number;
cluster: Cluster;
};
};
}
Loading