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
141 changes: 127 additions & 14 deletions src/evm/contracts/queries.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,132 @@
import { Protocol } from "@src/evm/enums";
import { releasesQueries } from "@src/evm/releases/queries";
import { createContractsQueries } from "@src/internal/factories/queries";
import type { Sablier } from "@src/types";
import _ from "lodash";
import { catalog } from "./catalog";

export const contractsQueries = createContractsQueries<
Sablier.EVM.Protocol,
Sablier.EVM.Contract,
Sablier.EVM.Deployment,
Sablier.EVM.Release,
Sablier.EVM.ContractCatalog
>({
catalog,
contractsField: "contracts",
normalizeAddress: (address) => address.toLowerCase(),
protocols: [Protocol.Airdrops, Protocol.Flow, Protocol.Legacy, Protocol.Lockup],
releasesQueries,
});
export const contractsQueries = {
/**
* Get a single contract using the following options:
*
* - { chainId, contractName, release }
* - { chainId, contractAddress, protocol }
* - { chainId, contractAddress, release }
* - { chainId, contractAddress, protocol, release }
*
* Note: If a contract address exists in multiple releases for the same protocol, you must specify the release.
*/
get: (opts: {
chainId: number;
contractAddress?: string;
contractName?: string;
protocol?: Sablier.EVM.Protocol;
release?: Sablier.EVM.Release;
}): Sablier.EVM.Contract | undefined => {
const { chainId, contractAddress, contractName, protocol, release } = opts;

// Validation
if (contractAddress && contractName) {
throw new Error("Sablier SDK: Cannot specify both contractAddress and contractName");
}

// Query by name requires release
if (contractName) {
if (!release) {
throw new Error("Sablier SDK: contractName requires release to be specified");
}
const dep = _.find(release.deployments, { chainId });
return dep ? _.find(dep.contracts, { name: contractName }) : undefined;
}

// Query by address
if (contractAddress) {
const address = contractAddress.toLowerCase();

// Scoped to specific release
if (release) {
const dep = _.find(release.deployments, { chainId });
return dep ? _.find(dep.contracts, (c) => c.address.toLowerCase() === address) : undefined;
}

// Scoped to protocol - check for duplicates across releases
if (protocol) {
const releases = releasesQueries.getAll({ protocol });
const matches = releases.filter((rel) => {
const dep = _.find(rel.deployments, { chainId });
return dep && _.some(dep.contracts, (c) => c.address.toLowerCase() === address);
});

if (matches.length > 1) {
const versions = matches.map((r) => r.version).join(", ");
throw new Error(
`Sablier SDK: Contract ${contractAddress} exists in multiple releases (${versions}) for "${protocol}". ` +
`Specify release: { chainId, contractAddress, release }`,
);
}

return _.get(catalog, [protocol, chainId, address]);
}

// Fallback: search all protocols
return (
_.get(catalog, [Protocol.Airdrops, chainId, address]) ||
_.get(catalog, [Protocol.Flow, chainId, address]) ||
_.get(catalog, [Protocol.Legacy, chainId, address]) ||
_.get(catalog, [Protocol.Lockup, chainId, address])
);
}

return undefined;
},
/**
* Get many contracts.
* - no options ⇒ all
* - { chainId } ⇒ all for that chain
* - { protocol } ⇒ all for that protocol
* - { protocol, chainId } ⇒ all for that protocol and chain
* - { release } ⇒ all deployments of that release
* - { release, chainId } ⇒ all for that release and chain
*/
getAll: (opts?: {
chainId?: number;
protocol?: Sablier.EVM.Protocol;
release?: Sablier.EVM.Release;
}): Sablier.EVM.Contract[] | undefined => {
const { protocol, chainId, release } = opts || {};

if (protocol && release) {
throw new Error("Sablier SDK: Cannot specify both protocol and release as query options");
}

// by protocol
if (protocol) {
const releases = releasesQueries.getAll({ protocol });
let deps = _.flatMap(releases, (r) => r.deployments);
if (chainId) {
deps = _.filter(deps, (d) => d.chainId === chainId);
if (deps.length === 0) return undefined;
}
return _.flatMap(deps, (d) => d.contracts);
}

// by explicit release
if (release) {
let deps = release.deployments;
if (chainId) {
deps = _.filter(deps, (d) => d.chainId === chainId);
if (deps.length === 0) return undefined;
}
return _.flatMap(deps, (d) => d.contracts);
}

// by chain id
if (chainId) {
const deps = _.flatMap(releasesQueries.getAll(), (r) => r.deployments);
const filtered = _.filter(deps, (d) => d.chainId === chainId);
return _.flatMap(filtered, (d) => d.contracts);
}

// no filters → all
return _.flatMap(releasesQueries.getAll(), (r) => r.deployments.flatMap((d) => d.contracts));
},
};
10 changes: 10 additions & 0 deletions src/evm/releases/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { chainsQueries } from "@src/evm/chains/queries";
import _ from "lodash";

export function getNestedValues<T extends Record<string, unknown>>(obj: T): string[] {
return _.flatMap(obj, (value) => {
if (_.isObject(value) && !_.isArray(value)) {
return getNestedValues(value as Record<string, unknown>);
}
return _.isString(value) ? value : [];
});
}

export function sortDeployments<T extends { chainId: number }>(deployments: T[]): T[] {
return deployments.sort((a, b) => {
Expand Down
52 changes: 47 additions & 5 deletions src/evm/releases/queries.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,51 @@
import { Protocol } from "@src/evm/enums";
import { createReleasesQueries } from "@src/internal/factories/queries";
import type { Sablier } from "@src/types";
import _ from "lodash";
import { releases } from "./data";

export const releasesQueries = createReleasesQueries<Sablier.EVM.Protocol, Sablier.EVM.Version, Sablier.EVM.Release>({
ProtocolEnum: Protocol,
releases: releases as Record<Sablier.EVM.Protocol, Record<string, Sablier.EVM.Release>>,
});
export const releasesQueries = {
get: (opts: { protocol: Sablier.EVM.Protocol; version: Sablier.EVM.Version }): Sablier.EVM.Release | undefined => {
const { protocol, version } = opts;
return _.get(releases, [protocol, version]);
},
/**
* Get all releases for a protocol.
* - {} ⇒ all releases for all protocols
* - {protocol} ⇒ all releases for that protocol
*/
getAll: (opts?: { protocol?: Sablier.EVM.Protocol }): Sablier.EVM.Release[] => {
const { protocol } = opts || {};
if (protocol) {
return _.flatMap(_.values(releases[protocol]));
}
// Recursively get all releases from all protocols in the enum
return _.flatMap(Object.values(Protocol), (protocolName) => _.flatMap(_.values(releases[protocolName])));
},
/**
* Get the first release:
* - {protocol} ⇒ first overall
* - {protocol,chainId} ⇒ first on that chain
*/
getFirst: (opts: { protocol: Sablier.EVM.Protocol; chainId?: number }): Sablier.EVM.Release | undefined => {
const { protocol, chainId } = opts;
const list = releases[protocol];

if (chainId) {
return _.find(list, (r) => _.some(r.deployments, { chainId }));
}

return _.values(list)[0];
},
/**
* Get the latest release for a protocol.
* - {protocol}
*/
getLatest: (opts: { protocol: Sablier.EVM.Protocol }): Sablier.EVM.Release => {
const list = _.values(releases[opts.protocol]);
const latest = list[list.length - 1];
if (!latest.isLatest) {
throw new Error(`Sablier SDK: No latest release found for Sablier ${opts.protocol}. Please report on GitHub.`);
}
return latest;
},
};
104 changes: 55 additions & 49 deletions src/evm/releases/resolvers.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { chainsQueries } from "@src/evm/chains/queries";
import { getNestedValues } from "@src/helpers";
import { createContractMapper, createStandardDeploymentResolver } from "@src/internal/factories/resolver";
import type { AliasMap } from "@src/shared/types";
import { getNestedValues } from "@src/evm/releases/helpers";
import { getContractExplorerURL } from "@src/helpers";
import type { Sablier } from "@src/types";
import _ from "lodash";

/* -------------------------------------------------------------------------- */
/* TYPES */
Expand All @@ -12,7 +12,7 @@ type DeploymentBaseParams = {
protocol: Sablier.EVM.Protocol;
version: Sablier.EVM.Version;
chainId: number;
aliasMap: AliasMap;
aliasMap: Sablier.EVM.AliasMap;
};

type DeploymentLockupV1Params = DeploymentBaseParams & {
Expand All @@ -28,49 +28,6 @@ type DeploymentStandardParams = DeploymentBaseParams & {

type ReleaseParams<T> = Omit<T, "kind" | "contractNames">;

/* -------------------------------------------------------------------------- */
/* PLATFORM SETUP */
/* -------------------------------------------------------------------------- */

const contractMapper = createContractMapper<
Sablier.EVM.Contract,
Sablier.EVM.Protocol,
Sablier.EVM.Version,
Sablier.EVM.Address
>(chainsQueries);

const standardDeploymentResolver = createStandardDeploymentResolver<
Sablier.EVM.Deployment,
Sablier.EVM.Contract,
Sablier.EVM.Protocol,
Sablier.EVM.Version,
Sablier.EVM.Address
>(contractMapper, "contracts");

/* -------------------------------------------------------------------------- */
/* EVM-SPECIFIC RESOLVERS */
/* -------------------------------------------------------------------------- */

/**
* Creates a LockupV1 deployment with separate core and periphery contracts
*/
function createLockupV1Deployment(params: DeploymentLockupV1Params): Sablier.EVM.Deployment.LockupV1 {
const { contractMap, ...baseParams } = params;

// Create standard deployment with merged contracts
const mergedContracts = { ...contractMap.core, ...contractMap.periphery };
const deployment = standardDeploymentResolver({
...baseParams,
contractMap: mergedContracts,
}) as Sablier.EVM.Deployment.LockupV1;

// Add separated core and periphery contracts
deployment.core = contractMapper(contractMap.core, baseParams);
deployment.periphery = contractMapper(contractMap.periphery, baseParams);

return deployment;
}

/* -------------------------------------------------------------------------- */
/* RESOLVERS */
/* -------------------------------------------------------------------------- */
Expand All @@ -81,14 +38,33 @@ export const resolvers = {
* Creates a LockupV1 deployment with separate core and periphery contracts
*/
lockupV1: (params: DeploymentLockupV1Params): Sablier.EVM.Deployment.LockupV1 => {
return createLockupV1Deployment(params);
const { contractMap, ...baseParams } = params;

// Create standard deployment with merged contracts
const mergedContracts = { ...contractMap.core, ...contractMap.periphery };
const deployment = resolvers.deployment.standard({
...baseParams,
contractMap: mergedContracts,
}) as Sablier.EVM.Deployment.LockupV1;

// Add separated core and periphery contracts
deployment.core = mapContractsToDeployment(contractMap.core, baseParams);
deployment.periphery = mapContractsToDeployment(contractMap.periphery, baseParams);

return deployment;
},

/**
* Creates a standard deployment with all contracts in a single array
*/
standard: (params: DeploymentStandardParams): Sablier.EVM.Deployment => {
return standardDeploymentResolver(params);
const { contractMap, ...baseParams } = params;
const contracts = mapContractsToDeployment(contractMap, baseParams);

return {
chainId: baseParams.chainId,
contracts,
};
},
},

Expand Down Expand Up @@ -116,3 +92,33 @@ export const resolvers = {
},
},
};

/* -------------------------------------------------------------------------- */
/* HELPERS */
/* -------------------------------------------------------------------------- */

/**
* Converts a contract map to an array of deployment contracts
*/
function mapContractsToDeployment(
contractMap: Sablier.EVM.ContractMap,
params: Pick<DeploymentBaseParams, "chainId" | "protocol" | "version" | "aliasMap">,
): Sablier.EVM.Contract[] {
const { chainId, protocol, version, aliasMap } = params;
const chain = chainsQueries.getOrThrow(chainId);

return _.entries(contractMap).map(([name, addressOrTuple]) => {
const [address, blockNumber] = Array.isArray(addressOrTuple) ? addressOrTuple : [addressOrTuple];

return {
address,
alias: aliasMap[name],
block: blockNumber,
chainId,
explorerURL: getContractExplorerURL(chain.blockExplorers.default.url, address),
name,
protocol,
version,
};
});
}
Loading