Skip to content
Open
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
47 changes: 42 additions & 5 deletions src/abi/calldata/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ function readULeb128(data: Uint8Array, index: {i: number}): bigint {
let accum = 0n;
let shouldContinue = true;
while (shouldContinue) {
if (index.i >= data.length) {
throw new Error("unexpected end of calldata while reading length");
}
const byte = data[index.i];
index.i++;
const rest = byte & 0x7f;
Expand All @@ -27,6 +30,9 @@ function decodeImpl(data: Uint8Array, index: {i: number}): CalldataEncodable {
case BigInt(consts.SPECIAL_FALSE):
return false;
case BigInt(consts.SPECIAL_ADDR): {
if (data.length - index.i < 20) {
throw new Error("unexpected end of calldata while reading address");
}
const res = data.slice(index.i, index.i + 20);
index.i += 20;
return new CalldataAddress(res);
Expand All @@ -36,20 +42,23 @@ function decodeImpl(data: Uint8Array, index: {i: number}): CalldataEncodable {
const rest = cur >> BigInt(consts.BITS_IN_TYPE);
switch (type) {
case consts.TYPE_BYTES: {
const ret = data.slice(index.i, index.i + Number(rest));
index.i += Number(rest);
const length = checkedRemainingLength(rest, data, index, "bytes");
const ret = data.slice(index.i, index.i + length);
index.i += length;
return ret;
}
case consts.TYPE_PINT:
return rest;
case consts.TYPE_NINT:
return -1n - rest;
case consts.TYPE_STR: {
const ret = data.slice(index.i, index.i + Number(rest));
index.i += Number(rest);
const length = checkedRemainingLength(rest, data, index, "string");
const ret = data.slice(index.i, index.i + length);
index.i += length;
return new TextDecoder("utf-8").decode(ret);
}
case consts.TYPE_ARR: {
ensureContainerCountFits(rest, data, index, "array");
const ret = [] as CalldataEncodable[];
let elems = rest;
while (elems > 0) {
Expand All @@ -59,11 +68,12 @@ function decodeImpl(data: Uint8Array, index: {i: number}): CalldataEncodable {
return ret;
}
case consts.TYPE_MAP: {
ensureContainerCountFits(rest, data, index, "map");
const ret = new Map<string, CalldataEncodable>();
let elems = rest;
while (elems > 0) {
elems--;
const strLen = Number(readULeb128(data, index));
const strLen = checkedRemainingLength(readULeb128(data, index), data, index, "map key");
const key = data.slice(index.i, index.i + strLen);
index.i += strLen;
const keyStr = new TextDecoder("utf-8").decode(key);
Expand All @@ -76,6 +86,33 @@ function decodeImpl(data: Uint8Array, index: {i: number}): CalldataEncodable {
}
}

function checkedRemainingLength(
length: bigint,
data: Uint8Array,
index: {i: number},
label: string,
): number {
const remaining = data.length - index.i;
if (length > BigInt(remaining)) {
throw new Error(`${label} length ${length} exceeds ${remaining} remaining calldata bytes`);
}
return Number(length);
}

function ensureContainerCountFits(
count: bigint,
data: Uint8Array,
index: {i: number},
label: string,
): void {
// Every encoded element consumes at least one byte. Reject impossible counts
// before allocating or iterating based on untrusted wire data.
const remaining = data.length - index.i;
if (count > BigInt(remaining)) {
throw new Error(`${label} element count ${count} exceeds ${remaining} remaining calldata bytes`);
}
}

export function decode(data: Uint8Array): CalldataEncodable {
const index = {i: 0};
const res = decodeImpl(data, index);
Expand Down
9 changes: 7 additions & 2 deletions src/abi/calldata/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ function reportError(msg: string, data: CalldataEncodable): never {

function toStringImplMap(data: Iterable<[string, CalldataEncodable]>, to: string[]) {
to.push("{");
let first = true;
for (const [k, v] of data) {
if (!first) {
to.push(",");
}
first = false;
to.push(JSON.stringify(k));
to.push(":");
toStringImpl(v, to);
Expand Down Expand Up @@ -48,7 +53,7 @@ function toStringImplMap(data: Iterable<[string, CalldataEncodable]>, to: string
if (data instanceof Uint8Array) {
to.push("b#");
for (const b of data) {
to.push(b.toString(16));
to.push(b.toString(16).padStart(2, "0"));
}
} else if (data instanceof Array) {
to.push("[");
Expand All @@ -62,7 +67,7 @@ function toStringImplMap(data: Iterable<[string, CalldataEncodable]>, to: string
} else if (data instanceof CalldataAddress) {
to.push("addr#");
for (const c of data.bytes) {
to.push(c.toString(16));
to.push(c.toString(16).padStart(2, "0"));
}
} else if (Object.getPrototypeOf(data) === Object.prototype) {
toStringImplMap(Object.entries(data), to);
Expand Down
17 changes: 13 additions & 4 deletions src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,10 +129,19 @@ const getCustomTransportConfig = (config: ClientConfig, chainConfig: GenLayerCha
* @returns Configured client with contract, transaction, and staking methods
*/
export const createClient = (config: ClientConfig = {chain: localnet}): GenLayerClient<GenLayerChain> => {
const chainConfig = config.chain || localnet;
if (config.endpoint) {
chainConfig.rpcUrls.default.http = [config.endpoint];
}
const configuredChain = config.chain || localnet;
const chainConfig = config.endpoint
? {
...configuredChain,
rpcUrls: {
...configuredChain.rpcUrls,
default: {
...configuredChain.rpcUrls.default,
http: [config.endpoint],
},
},
}
: configuredChain;

const customTransport = custom(getCustomTransportConfig(config, chainConfig as GenLayerChain), {retryCount: 0, retryDelay: 0});
const publicClient = createPublicClient(chainConfig as GenLayerChain, customTransport).extend(
Expand Down
22 changes: 12 additions & 10 deletions src/contracts/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,10 @@ function validateValueAgainstType(
return typeof value === 'string' || value instanceof Uint8Array;
}
if (type === 'address') return typeof value === 'string';
if (type === 'int') return typeof value === 'number' || typeof value === 'bigint';
if (type === 'int') {
return typeof value === 'bigint' ||
(typeof value === 'number' && Number.isFinite(value) && Number.isInteger(value));
}
if (type === 'array') return Array.isArray(value);
if (type === 'dict') return isPlainObject(value) || value instanceof Map;

Expand Down Expand Up @@ -99,15 +102,14 @@ function validateValueAgainstType(
}

// Struct-like dict schema: validate each provided key that exists in schema.
if (isPlainObject(value)) {
return Object.entries(type).every(([key, keyType]) => {
if (!(key in value)) return true;
return validateValueAgainstType(
value[key],
keyType as ContractParamsSchema,
);
});
}
if (!isPlainObject(value)) return false;
return Object.entries(type).every(([key, keyType]) => {
if (!(key in value)) return true;
return validateValueAgainstType(
value[key],
keyType as ContractParamsSchema,
);
});
}

return true;
Expand Down
8 changes: 5 additions & 3 deletions src/staking/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {getContract, decodeEventLog, PublicClient, Client, Transport, Chain, Account, Address as ViemAddress, GetContractReturnType, toHex, encodeFunctionData, BaseError, ContractFunctionRevertedError, decodeErrorResult, RawContractError} from "viem";
import {getContract, decodeEventLog, PublicClient, Client, Transport, Chain, Account, Address as ViemAddress, GetContractReturnType, toHex, isHex, encodeFunctionData, BaseError, ContractFunctionRevertedError, decodeErrorResult, RawContractError} from "viem";
import {GenLayerClient, GenLayerChain, Address} from "@/types";
import {STAKING_ABI, VALIDATOR_WALLET_ABI} from "@/abi/staking";
import {parseStakingAmount, formatStakingAmount} from "./utils";
Expand Down Expand Up @@ -470,7 +470,9 @@ export const stakingActions = (
twitter: identityRaw.twitter,
telegram: identityRaw.telegram,
github: identityRaw.github,
extraCid: identityRaw.extraCid ? toHex(identityRaw.extraCid) : "",
extraCid: identityRaw.extraCid
? (isHex(identityRaw.extraCid) ? identityRaw.extraCid : toHex(identityRaw.extraCid))
: "",
};
}

Expand Down Expand Up @@ -547,7 +549,7 @@ export const stakingActions = (
},

/** Checks whether a validator's self-stake is below the configured validator minimum. */
isValidatorBelowMin: async (validator: Address): Promise<boolean> => {
isValidatorBelowMinStake: async (validator: Address): Promise<boolean> => {
const contract = getReadOnlyStakingContract();
const [view, minStake] = await Promise.all([
contract.read.validatorView([validator as ViemAddress]) as Promise<{vStake: bigint}>,
Expand Down
10 changes: 9 additions & 1 deletion src/transactions/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,18 @@ export const isSuccessful = (transaction: GenLayerTransaction): boolean => {
? undefined
: executionResultNumberToName[String(transaction.txExecutionResult) as keyof typeof executionResultNumberToName]
);
const studioExecutionSucceeded =
executionResultName === undefined &&
transaction.consensus_data?.leader_receipt?.some(
receipt => receipt.execution_result === "SUCCESS",
) === true;

return (
(statusName === TransactionStatus.ACCEPTED || statusName === TransactionStatus.FINALIZED) &&
executionResultName === ExecutionResult.FINISHED_WITH_RETURN
(
executionResultName === ExecutionResult.FINISHED_WITH_RETURN ||
studioExecutionSucceeded
)
);
};

Expand Down
1 change: 1 addition & 0 deletions src/types/staking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export interface StakingActions {
delegatorExit: (options: DelegatorExitOptions) => Promise<StakingTransactionResult>;
delegatorClaim: (options: DelegatorClaimOptions) => Promise<StakingTransactionResult>;
isValidator: (address: Address) => Promise<boolean>;
isValidatorBelowMinStake: (validator: Address) => Promise<boolean>;
getValidatorInfo: (validator: Address) => Promise<ValidatorInfo>;
getStakeInfo: (delegator: Address, validator: Address) => Promise<StakeInfo>;
getEpochInfo: () => Promise<EpochInfo>;
Expand Down
52 changes: 29 additions & 23 deletions src/utils/jsonifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,41 +87,47 @@ function _toJsonSafeDeep(value: CalldataEncodable, seen: WeakSet<object>): any {

// Objects and structured values
if (typeof value === "object") {
if (seen.has(value as object)) {
// Prevent potential cycles; represent as null
return null;
}
seen.add(value as object);

if (value instanceof Uint8Array) {
return toHex(value);
}

if (value instanceof Array) {
return value.map((v) => _toJsonSafeDeep(v as CalldataEncodable, seen));
if (value instanceof CalldataAddress) {
return toHex(value.bytes);
}

if (value instanceof Map) {
const obj: Record<string, any> = {};
for (const [k, v] of value.entries()) {
obj[k] = _toJsonSafeDeep(v as CalldataEncodable, seen);
}
return obj;
if (seen.has(value as object)) {
// Prevent potential cycles; represent as null
return null;
}
seen.add(value as object);

if (value instanceof CalldataAddress) {
return toHex(value.bytes);
}
try {
if (value instanceof Array) {
return value.map((v) => _toJsonSafeDeep(v as CalldataEncodable, seen));
}

if (Object.getPrototypeOf(value) === Object.prototype) {
const obj: Record<string, any> = {};
for (const [k, v] of Object.entries(value)) {
obj[k] = _toJsonSafeDeep(v as CalldataEncodable, seen);
if (value instanceof Map) {
const obj: Record<string, any> = {};
for (const [k, v] of value.entries()) {
obj[k] = _toJsonSafeDeep(v as CalldataEncodable, seen);
}
return obj;
}
return obj;

if (Object.getPrototypeOf(value) === Object.prototype) {
const obj: Record<string, any> = {};
for (const [k, v] of Object.entries(value)) {
obj[k] = _toJsonSafeDeep(v as CalldataEncodable, seen);
}
return obj;
}
} finally {
// Track only the active recursion path. Shared references in an acyclic
// object graph must be serialized each time they occur.
seen.delete(value as object);
}
}

// Fallback: return as-is (shouldn't normally reach here)
return value as any;
}
}
23 changes: 20 additions & 3 deletions src/vesting/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
decodeErrorResult,
encodeFunctionData,
getContract,
isHex,
PublicClient,
RawContractError,
toHex,
Expand Down Expand Up @@ -94,10 +95,26 @@ function extractRevertReason(err: unknown): string {

function encodeExtraCid(extraCid?: string): `0x${string}` {
if (!extraCid) return "0x";
if (extraCid.startsWith("0x")) return extraCid as `0x${string}`;
if (extraCid.startsWith("0x")) {
if (!isHex(extraCid, {strict: true}) || extraCid.length % 2 !== 0) {
throw new Error("extraCid must be a valid even-length hex string");
}
return extraCid;
}
return toHex(new TextEncoder().encode(extraCid));
}

function parseExitShares(shares: bigint | string): bigint {
if (typeof shares === "string" && shares.trim() === "") {
throw new Error("shares must not be empty");
}
const parsed = typeof shares === "string" ? BigInt(shares) : shares;
if (parsed <= 0n) {
throw new Error("shares must be greater than zero");
}
return parsed;
}

export const vestingActions = (
client: GenLayerClient<GenLayerChain>,
publicClient: PublicClient,
Expand Down Expand Up @@ -307,7 +324,7 @@ export const vestingActions = (

/** Exits a vesting contract's delegation by burning shares. Must be called by the vesting beneficiary. */
vestingDelegatorExit: async (options: VestingDelegatorExitOptions): Promise<VestingTransactionResult> => {
const shares = typeof options.shares === "string" ? BigInt(options.shares) : options.shares;
const shares = parseExitShares(options.shares);
const data = encodeFunctionData({
abi: VESTING_ABI,
functionName: "vestingDelegatorExit",
Expand Down Expand Up @@ -359,7 +376,7 @@ export const vestingActions = (

/** Exits validator self-stake by burning shares from a vesting-owned validator wallet. */
vestingValidatorExit: async (options: VestingValidatorExitOptions): Promise<VestingTransactionResult> => {
const shares = typeof options.shares === "string" ? BigInt(options.shares) : options.shares;
const shares = parseExitShares(options.shares);
const data = encodeFunctionData({
abi: VESTING_ABI,
functionName: "vestingValidatorExit",
Expand Down
Loading
Loading