zksync-sso
Version:
ZKsync Smart Sign On SDK
230 lines (208 loc) • 8.7 kB
text/typescript
import { type Abi, type AbiFunction, type AbiStateMutability, type Address, type ContractFunctionArgs, type ContractFunctionName, encodeAbiParameters, getAddress, toFunctionSelector, toHex } from "viem";
import { ConstraintCondition, type Limit, LimitType, LimitUnlimited, LimitZero, type SessionConfig } from "../../utils/session.js";
import type { ContractWriteMutability, IndexedValues } from "./type-utils.js";
import { encodedInputToAbiChunks, getParameterChunkIndex, isDynamicInputType, isFollowedByDynamicInputType, msStringToSeconds } from "./utils.js";
export type PartialLimit = bigint | {
limit: bigint;
period?: string | bigint;
} | {
limitType: "lifetime" | LimitType.Lifetime;
limit: bigint;
} | {
limitType: "unlimited" | LimitType.Unlimited;
} | {
limitType: "allowance" | LimitType.Allowance;
limit: bigint;
period: string | bigint;
};
export type PartialCallPolicy = {
address: Address;
abi: Abi;
functionName: string;
maxValuePerUse?: bigint;
valueLimit?: PartialLimit;
constraints?: {
index: number;
value?: unknown;
condition?: keyof typeof ConstraintCondition;
limit?: PartialLimit;
}[];
};
export interface TypesafePartialCallPolicy<
abi extends Abi,
functionName extends ContractFunctionName<abi, ContractWriteMutability>,
> extends PartialCallPolicy {
abi: abi;
functionName: functionName;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
constraints?: (IndexedValues<ContractFunctionArgs<abi, ContractWriteMutability, functionName>>[number] & {
condition?: keyof typeof ConstraintCondition;
limit?: PartialLimit;
})[];
};
// Typesafety helper function
export const callPolicy = <
abi extends Abi,
functionName extends ContractFunctionName<abi, ContractWriteMutability>,
>(
policy: TypesafePartialCallPolicy<abi, functionName>,
): PartialCallPolicy => policy as PartialCallPolicy;
export type PartialTransferPolicy = {
to: Address;
maxValuePerUse?: bigint;
valueLimit?: PartialLimit;
};
export interface SessionPreferences {
expiry?: string | bigint | Date;
feeLimit?: PartialLimit;
contractCalls?: PartialCallPolicy[];
transfers?: PartialTransferPolicy[];
};
export const formatLimitPreferences = (limit: PartialLimit): Limit => {
/* Just bigint was passed */
if (typeof limit === "bigint") {
return {
limitType: LimitType.Lifetime,
limit,
period: 0n,
};
}
/* LimitType was specified */
if ("limitType" in limit) {
if (limit.limitType === "lifetime" || limit.limitType === LimitType.Lifetime) {
return {
limitType: LimitType.Lifetime,
limit: limit.limit,
period: 0n,
};
} else if (limit.limitType === "unlimited" || limit.limitType === LimitType.Unlimited) {
return {
limitType: LimitType.Unlimited,
limit: 0n,
period: 0n,
};
} else if (limit.limitType === "allowance" || limit.limitType === LimitType.Allowance) {
return {
limitType: LimitType.Allowance,
limit: limit.limit,
period: typeof limit.period === "string" ? msStringToSeconds(limit.period) : limit.period,
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
throw new Error(`Invalid limit type: ${(limit as any).limitType}`);
}
/* LimitType not selected */
if (limit.period) {
return {
limitType: LimitType.Allowance,
limit: limit.limit,
period: typeof limit.period === "string" ? msStringToSeconds(limit.period) : limit.period,
};
}
return {
limitType: LimitType.Lifetime,
limit: limit.limit,
period: 0n,
};
};
export const formatDatePreferences = (date: string | bigint | Date): bigint => {
if (typeof date === "string") {
const now = Math.floor(new Date().getTime() / 1000);
const seconds = msStringToSeconds(date);
return BigInt(now) + seconds;
}
if (date instanceof Date) {
const seconds = Math.floor(date.getTime() / 1000);
return BigInt(seconds);
}
return date;
};
export function formatSessionPreferences(
preferences: SessionPreferences,
defaults: {
expiresAt: bigint;
feeLimit: Limit;
},
): Omit<SessionConfig, "signer"> {
return {
expiresAt: preferences.expiry ? formatDatePreferences(preferences.expiry) : defaults.expiresAt,
feeLimit: preferences.feeLimit ? formatLimitPreferences(preferences.feeLimit) : defaults.feeLimit,
callPolicies: preferences.contractCalls?.map((policy) => {
const allowedStateMutability: ContractWriteMutability[] = ["nonpayable", "payable"];
const abiFunction = (policy.abi as Abi).find((fn) => fn.type === "function" && fn.name === policy.functionName && (allowedStateMutability as AbiStateMutability[]).includes(fn.stateMutability)) as AbiFunction;
if (!abiFunction) throw new Error(`Function not found in the provided ABI: ${policy.functionName}`);
const selector = toFunctionSelector(abiFunction);
const valueLimit = policy.valueLimit ? formatLimitPreferences(policy.valueLimit) : LimitZero;
return {
target: getAddress(policy.address.toLowerCase()),
maxValuePerUse: policy.maxValuePerUse ?? valueLimit.limit,
valueLimit,
selector: selector,
constraints: policy.constraints?.map((constraint) => {
const limit = constraint.limit ? formatLimitPreferences(constraint.limit) : LimitUnlimited;
let condition = ConstraintCondition.Unconstrained;
if (constraint.condition) {
condition = ConstraintCondition[constraint.condition];
} else if (constraint.value !== undefined && constraint.value !== null) {
condition = ConstraintCondition.Equal;
}
const input = abiFunction.inputs[constraint.index];
if (!input) {
throw new Error(`Target function parameter not found in the provided ABI function. Provided at function ${policy.functionName}, index ${constraint.index}`);
}
const isDynamicType = isDynamicInputType(input.type);
if (isDynamicType) {
throw new Error(`Function parameters with dynamic types are not supported for constraint validation. Provided at function ${policy.functionName}, index ${constraint.index}`);
}
const isFollowedByDynamicType = isFollowedByDynamicInputType(abiFunction, constraint.index);
if (isFollowedByDynamicType) {
throw new Error(`Target function parameter is followed by a dynamic type parameter. Provided at function ${policy.functionName}, index ${constraint.index}`);
}
const startingAbiChunkIndex = getParameterChunkIndex(abiFunction, constraint.index);
if (constraint.value === undefined || constraint.value === null) {
return {
index: BigInt(startingAbiChunkIndex),
condition: ConstraintCondition.Unconstrained,
refValue: toHex("", { size: 32 }),
limit,
};
}
const encodedInput = encodeAbiParameters([input], [constraint.value]);
const abiBytesChunks = encodedInputToAbiChunks(encodedInput);
const ALLOWED_OVERFLOW_CONDITIONS: ConstraintCondition[] = [
ConstraintCondition.Unconstrained,
ConstraintCondition.Equal,
ConstraintCondition.NotEqual,
];
const ALLOWED_OVERFLOW_LIMIT_TYPES: LimitType[] = [
LimitType.Unlimited,
];
if (
abiBytesChunks.length > 1
&& (
!ALLOWED_OVERFLOW_CONDITIONS.includes(condition) // Can't validate condition (e.g. < >) if value is split across multiple chunks
|| !ALLOWED_OVERFLOW_LIMIT_TYPES.includes(limit.limitType) // Can't validate limit if value is split across multiple chunks
)
) {
throw new Error(`Encoded input size of parameter at index ${constraint.index} of ${policy.functionName} exceeds the maximum size of 32 bytes: ${abiBytesChunks.length * 32} bytes`);
};
return abiBytesChunks.map((abiChunk, index) => ({
index: BigInt(startingAbiChunkIndex + index),
condition,
refValue: abiChunk,
limit,
}));
}).flat() ?? [],
};
}) ?? [],
transferPolicies: preferences.transfers?.map((policy) => {
const valueLimit = policy.valueLimit ? formatLimitPreferences(policy.valueLimit) : LimitZero;
return {
target: getAddress(policy.to.toLowerCase()),
maxValuePerUse: policy.maxValuePerUse ?? valueLimit.limit,
valueLimit,
};
}) ?? [],
};
}