Skip to content

Commit 72181ed

Browse files
feat: support for solana contracts and releases
1 parent 6b4779b commit 72181ed

File tree

24 files changed

+632
-0
lines changed

24 files changed

+632
-0
lines changed

src/sablier.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { chainsQueries as evmChainsQueries } from "./evm/chains/queries";
2828
import { contractsQueries as evmContractsQueries } from "./evm/contracts/queries";
2929
import { releasesQueries as evmReleasesQueries } from "./evm/releases/queries";
3030
import { chainsQueries as solanaChainsQueries } from "./solana/chains/queries";
31+
import { contractsQueries as solanaContractsQueries } from "./solana/contracts/queries";
32+
import { releasesQueries as solanaReleasesQueries } from "./solana/releases/queries";
3133

3234
/**
3335
* Has to be defined here to avoid circular dependencies.
@@ -47,6 +49,21 @@ const evmDeploymentsQueries = {
4749
},
4850
};
4951

52+
const solanaDeploymentsQueries = {
53+
/**
54+
* Get many deployments.
55+
* - default ⇒ all across all releases
56+
* - release ⇒ that release's deployments
57+
*/
58+
get: (opts: { chainId: number; release: Sablier.Solana.Release }): Sablier.Solana.Deployment | undefined => {
59+
const { release, chainId } = opts || {};
60+
return _.find(release.deployments, { chainId });
61+
},
62+
getAll: (): Sablier.Solana.Deployment[] => {
63+
return _.flatMap(solanaReleasesQueries.getAll(), (r) => r.deployments);
64+
},
65+
};
66+
5067
const evm = {
5168
chains: evmChainsQueries,
5269
contracts: evmContractsQueries,
@@ -56,6 +73,9 @@ const evm = {
5673

5774
const solana = {
5875
chains: solanaChainsQueries,
76+
contracts: solanaContractsQueries,
77+
deployments: solanaDeploymentsQueries,
78+
releases: solanaReleasesQueries,
5979
};
6080

6181
export const sablier = {

src/solana/contracts/catalog.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Protocol } from "@src/solana/enums";
2+
import { releasesQueries } from "@src/solana/releases/queries";
3+
import type { Sablier } from "@src/types";
4+
import _ from "lodash";
5+
6+
function getCatalog(): Sablier.Solana.ContractCatalog {
7+
const catalog: Sablier.Solana.ContractCatalog = {
8+
[Protocol.Airdrops]: {},
9+
[Protocol.Lockup]: {},
10+
};
11+
12+
for (const release of releasesQueries.getAll()) {
13+
const { protocol, version, deployments } = release;
14+
15+
for (const deployment of deployments) {
16+
const { chainId, contracts } = deployment;
17+
18+
for (const contract of contracts) {
19+
const address = contract.address;
20+
const entry = _.merge(contract, {
21+
protocol,
22+
version,
23+
});
24+
_.set(catalog, [protocol, chainId, address], entry);
25+
}
26+
}
27+
}
28+
return catalog;
29+
}
30+
31+
export const catalog = getCatalog();

src/solana/contracts/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { catalog } from "./catalog";
2+
import { names } from "./names";
3+
4+
export const contracts = {
5+
catalog,
6+
names,
7+
};

src/solana/contracts/names.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import airdropsV1_0 from "@src/solana/releases/airdrops/v1.0/manifest";
2+
import lockupV1_0 from "@src/solana/releases/lockup/v1.0/manifest";
3+
import type { Sablier } from "@src/types";
4+
import _ from "lodash";
5+
6+
/**
7+
* Works at compile-time!
8+
*/
9+
type LeafKeys<T> = keyof T;
10+
11+
type A1_0 = LeafKeys<typeof airdropsV1_0>;
12+
type L1_0 = LeafKeys<typeof lockupV1_0>;
13+
14+
// Final exported type: only these known keys allowed
15+
type ContractNames = Record<A1_0 | L1_0, string>;
16+
17+
function flatten(manifest: Sablier.Solana.Manifest): Record<string, string> {
18+
return { ...manifest } as Record<string, string>;
19+
}
20+
21+
/**
22+
* Flatten & merge across all releases
23+
* @example
24+
* ```ts
25+
* const lockupName = names.SABLIER_LOCKUP; // "SablierLockup"
26+
* const flowName = names.SABLIER_FLOW; //"SablierFlow"
27+
* ```
28+
*/
29+
function getNames(): ContractNames {
30+
const manifests = [airdropsV1_0, lockupV1_0];
31+
32+
const flattened = manifests.map(flatten);
33+
return _.merge({}, ...flattened) as ContractNames;
34+
}
35+
36+
export const names = getNames();

src/solana/contracts/queries.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { Protocol } from "@src/solana/enums";
2+
import { releasesQueries } from "@src/solana/releases/queries";
3+
import type { Sablier } from "@src/types";
4+
import _ from "lodash";
5+
import { catalog } from "./catalog";
6+
7+
export const contractsQueries = {
8+
/**
9+
* Get a single contract using the following options:
10+
*
11+
* - { chainId, contractName, release }
12+
* - { chainId, contractAddress, protocol }
13+
* - { chainId, contractAddress, release }
14+
* - { chainId, contractAddress, protocol, release }
15+
*
16+
* Note: If a contract address exists in multiple releases for the same protocol, you must specify the release.
17+
*/
18+
get: (opts: {
19+
chainId: number;
20+
contractAddress?: string;
21+
contractName?: string;
22+
protocol?: Sablier.Solana.Protocol;
23+
release?: Sablier.Solana.Release;
24+
}): Sablier.Solana.Contract | undefined => {
25+
const { chainId, contractAddress, contractName, protocol, release } = opts;
26+
27+
// Validation
28+
if (contractAddress && contractName) {
29+
throw new Error("Sablier SDK: Cannot specify both contractAddress and contractName");
30+
}
31+
32+
// Query by name requires release
33+
if (contractName) {
34+
if (!release) {
35+
throw new Error("Sablier SDK: contractName requires release to be specified");
36+
}
37+
const dep = _.find(release.deployments, { chainId });
38+
return dep ? _.find(dep.contracts, { name: contractName }) : undefined;
39+
}
40+
41+
// Query by address
42+
if (contractAddress) {
43+
// Scoped to specific release
44+
if (release) {
45+
const dep = _.find(release.deployments, { chainId });
46+
return dep ? _.find(dep.contracts, (c) => c.address === contractAddress) : undefined;
47+
}
48+
49+
// Scoped to protocol - check for duplicates across releases
50+
if (protocol) {
51+
const releases = releasesQueries.getAll({ protocol });
52+
const matches = releases.filter((rel) => {
53+
const dep = _.find(rel.deployments, { chainId });
54+
return dep && _.some(dep.contracts, (c) => c.address === contractAddress);
55+
});
56+
57+
if (matches.length > 1) {
58+
const versions = matches.map((r) => r.version).join(", ");
59+
throw new Error(
60+
`Sablier SDK: Contract ${contractAddress} exists in multiple releases (${versions}) for "${protocol}". ` +
61+
`Specify release: { chainId, contractAddress, release }`,
62+
);
63+
}
64+
65+
return _.get(catalog, [protocol, chainId, contractAddress]);
66+
}
67+
68+
// Fallback: search all protocols
69+
return (
70+
_.get(catalog, [Protocol.Airdrops, chainId, contractAddress]) ||
71+
_.get(catalog, [Protocol.Lockup, chainId, contractAddress])
72+
);
73+
}
74+
75+
return undefined;
76+
},
77+
/**
78+
* Get many contracts.
79+
* - no options ⇒ all
80+
* - { chainId } ⇒ all for that chain
81+
* - { protocol } ⇒ all for that protocol
82+
* - { protocol, chainId } ⇒ all for that protocol and chain
83+
* - { release } ⇒ all deployments of that release
84+
* - { release, chainId } ⇒ all for that release and chain
85+
*/
86+
getAll: (opts?: {
87+
chainId?: number;
88+
protocol?: Sablier.Solana.Protocol;
89+
release?: Sablier.Solana.Release;
90+
}): Sablier.Solana.Contract[] | undefined => {
91+
const { protocol, chainId, release } = opts || {};
92+
93+
if (protocol && release) {
94+
throw new Error("Sablier SDK: Cannot specify both protocol and release as query options");
95+
}
96+
97+
// by protocol
98+
if (protocol) {
99+
const releases = releasesQueries.getAll({ protocol });
100+
let deps = _.flatMap(releases, (r) => r.deployments);
101+
if (chainId) {
102+
deps = _.filter(deps, (d) => d.chainId === chainId);
103+
if (deps.length === 0) return undefined;
104+
}
105+
return _.flatMap(deps, (d) => d.contracts);
106+
}
107+
108+
// by explicit release
109+
if (release) {
110+
let deps = release.deployments;
111+
if (chainId) {
112+
deps = _.filter(deps, (d) => d.chainId === chainId);
113+
if (deps.length === 0) return undefined;
114+
}
115+
return _.flatMap(deps, (d) => d.contracts);
116+
}
117+
118+
// by chain id
119+
if (chainId) {
120+
const deps = _.flatMap(releasesQueries.getAll(), (r) => r.deployments);
121+
const filtered = _.filter(deps, (d) => d.chainId === chainId);
122+
return _.flatMap(filtered, (d) => d.contracts);
123+
}
124+
125+
// no filters → all
126+
return _.flatMap(releasesQueries.getAll(), (r) => r.deployments.flatMap((d) => d.contracts));
127+
},
128+
};

src/solana/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
export { chains } from "./chains";
2+
export * from "./contracts";
3+
export * from "./enums";
24
export * from "./idl";
5+
export * from "./releases";
36
export * from "./types";
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { Sablier } from "@src/types";
2+
import { release as releaseV1_0 } from "./v1.0";
3+
4+
export const airdrops: Record<Sablier.Solana.Version.Airdrops, Sablier.Solana.Release> = {
5+
"v1.0": releaseV1_0,
6+
};
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import manifest from "./manifest";
2+
3+
const aliases = {
4+
[manifest.SABLIER_MERKLE_INSTANT]: "merkleFactoryInstant",
5+
};
6+
7+
export default aliases;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { chains } from "@src/solana/chains";
2+
import { Protocol } from "@src/solana/enums";
3+
import { resolvers } from "@src/solana/releases/resolvers";
4+
import type { Sablier } from "@src/types";
5+
import aliases from "./aliases";
6+
import manifest from "./manifest";
7+
8+
function get(chainId: number, contractMap: Sablier.Solana.ContractMap): Sablier.Solana.Deployment {
9+
return resolvers.deployment.standard({
10+
aliasMap: aliases,
11+
chainId,
12+
contractMap,
13+
protocol: Protocol.Airdrops,
14+
version: "v1.0",
15+
});
16+
}
17+
18+
/**
19+
* @description Mainnet deployments for Airdrops v1.0
20+
*/
21+
export const mainnets: Sablier.Solana.Deployment[] = [
22+
get(chains.solana_mainnetBeta.id, {
23+
[manifest.SABLIER_MERKLE_INSTANT]: ["7XrxoQejBoGouW4V3aozTSwub7xSDjYqB4Go7YLjF9rV", 365675848],
24+
}),
25+
];
26+
27+
/**
28+
* @description Testnet deployments for Airdrops v1.0
29+
*/
30+
export const testnets: Sablier.Solana.Deployment[] = [
31+
get(chains.solana_devnet.id, {
32+
[manifest.SABLIER_MERKLE_INSTANT]: ["7XrxoQejBoGouW4V3aozTSwub7xSDjYqB4Go7YLjF9rV", 408019095],
33+
}),
34+
];
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { SablierMerkleInstantV10IDL } from "@src/solana/idl/airdrops/v1.0";
2+
import type { Sablier } from "@src/types";
3+
import manifest from "./manifest";
4+
5+
export const idl: Sablier.Solana.IdlMap = {
6+
[manifest.SABLIER_MERKLE_INSTANT]: SablierMerkleInstantV10IDL,
7+
};

0 commit comments

Comments
 (0)