Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
32 changes: 32 additions & 0 deletions packages/delegation-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,35 @@ export const toHexString = ({
}): string => {
return value.toString(16).padStart(size * 2, '0');
};

/**
* Validates if a string is a properly formatted hex string.
* @param value - The string to validate.
* @param options - Optional validation options.
* @param options.minLength - Minimum length after '0x' prefix (default: 1).
* @param options.exactLength - Exact length after '0x' prefix (optional).
* @returns True if the string is a valid hex string, false otherwise.
*/
export function isValidHex(
Comment thread
jeffsmale90 marked this conversation as resolved.
Outdated
value: string,
options: { minLength?: number; exactLength?: number } = {},
): boolean {
const { minLength = 1, exactLength } = options;

if (typeof value !== 'string' || !value.startsWith('0x')) {
return false;
}

const hexContent = value.slice(2); // Remove '0x' prefix

if (exactLength !== undefined && hexContent.length !== exactLength) {
return false;
}

if (hexContent.length < minLength) {
return false;
}

// Check if all characters are valid hex (0-9, a-f, A-F)
return /^[0-9a-fA-F]*$/u.test(hexContent);
}
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { isStrictHexString } from '@metamask/utils';
import { describe, it, expect } from 'vitest';

import { createNativeTokenPeriodTransferTerms } from '../../src/caveats/nativeTokenPeriodTransfer';
Expand Down Expand Up @@ -53,7 +54,8 @@ describe('createNativeTokenPeriodTransferTerms', () => {
});

expect(result).toHaveLength(194);
expect(result).toMatch(/^0x[0-9a-f]{192}$/u);
expect(isStrictHexString(result)).toBe(true);
expect(result.length).toBe(194); // Additional length validation
});

it('creates valid terms for maximum safe values', () => {
Expand All @@ -68,7 +70,8 @@ describe('createNativeTokenPeriodTransferTerms', () => {
});

expect(result).toHaveLength(194);
expect(result).toMatch(/^0x[0-9a-f]{192}$/u);
expect(isStrictHexString(result)).toBe(true);
expect(result.length).toBe(194); // Additional length validation
});

it('throws an error for zero period amount', () => {
Expand Down Expand Up @@ -225,8 +228,9 @@ describe('createNativeTokenPeriodTransferTerms', () => {
startDate,
});

expect(result).toMatch(/^0x[0-9a-f]{192}$/u);
expect(isStrictHexString(result)).toBe(true);
expect(result).toHaveLength(194);
expect(result.length).toBe(194); // Additional length validation
});

// Tests for bytes return type
Expand Down
1 change: 1 addition & 0 deletions packages/delegation-toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"@metamask/eslint-config": "^12.0.0",
"@metamask/eslint-config-nodejs": "^12.0.0",
"@metamask/eslint-config-typescript": "^12.0.0",
"@metamask/utils": "^11.4.0",
"@types/node": "^20.10.6",
"@types/sinon": "^17.0.3",
"@typescript-eslint/eslint-plugin": "^5.42.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { DelegationManager } from '@metamask/delegation-abis';
import type { Address, Client, Hex } from 'viem';
import { readContract } from 'viem/actions';

export type ReadDisabledDelegationsParameters = {
client: Client;
contractAddress: Address;
delegationHash: Hex;
};

export const read = async ({
client,
contractAddress,
delegationHash,
}: ReadDisabledDelegationsParameters) =>
await readContract(client, {
address: contractAddress,
abi: DelegationManager.abi,
functionName: 'disabledDelegations',
args: [delegationHash],
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { read as disabledDelegations } from './methods/disabledDelegations';
import { read as getAnyDelegate } from './methods/getAnyDelegate';
import { read as getRootAuthority } from './methods/getRootAuthority';

export { getAnyDelegate, getRootAuthority };
export { getAnyDelegate, getRootAuthority, disabledDelegations };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as read from './read';

export { read };
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ERC20PeriodTransferEnforcer } from '@metamask/delegation-abis';
import type { Address, Client, Hex } from 'viem';
import { readContract } from 'viem/actions';

export type ReadGetAvailableAmountParameters = {
client: Client;
contractAddress: Address;
delegationHash: Hex;
delegationManager: Address;
terms: Hex;
};

export const read = async ({
client,
contractAddress,
delegationHash,
delegationManager,
terms,
}: ReadGetAvailableAmountParameters) => {
const [availableAmount, isNewPeriod, currentPeriod] = await readContract(
client,
{
address: contractAddress,
abi: ERC20PeriodTransferEnforcer.abi,
functionName: 'getAvailableAmount',
args: [delegationHash, delegationManager, terms],
},
);

return {
availableAmount,
isNewPeriod,
currentPeriod,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { read as getAvailableAmount } from './methods/getAvailableAmount';

export { getAvailableAmount };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as read from './read';

export { read };
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ERC20StreamingEnforcer } from '@metamask/delegation-abis';
import type { Address, Client, Hex } from 'viem';
import { readContract, getBlock } from 'viem/actions';

export type ReadGetAvailableAmountParameters = {
client: Client;
contractAddress: Address;
delegationManager: Address;
delegationHash: Hex;
terms: Hex;
};

export const read = async ({
client,
contractAddress,
delegationManager,
delegationHash,
terms,
}: ReadGetAvailableAmountParameters) => {
// Get current block timestamp from blockchain
const currentBlock = await getBlock(client);
const currentTimestamp = currentBlock.timestamp;

// First, get the current state from the contract
const allowanceState = await readContract(client, {
address: contractAddress,
abi: ERC20StreamingEnforcer.abi,
functionName: 'streamingAllowances',
args: [delegationManager, delegationHash],
});

const [initialAmount, maxAmount, amountPerSecond, startTime, spent] =
allowanceState;

// Check if state exists (startTime != 0)
if (startTime !== 0n) {
// State exists, calculate available amount using the stored state
const availableAmount = getAvailableAmount({
initialAmount,
maxAmount,
amountPerSecond,
startTime,
spent,
currentTimestamp,
});

return {
availableAmount,
};
}

// State doesn't exist, decode terms and simulate with spent = 0
const decodedTerms = await readContract(client, {
address: contractAddress,
abi: ERC20StreamingEnforcer.abi,
functionName: 'getTermsInfo',
args: [terms],
});

const [
,
decodedInitialAmount,
decodedMaxAmount,
decodedAmountPerSecond,
decodedStartTime,
] = decodedTerms;

// Simulate using decoded terms with spent = 0
const availableAmount = getAvailableAmount({
initialAmount: decodedInitialAmount,
maxAmount: decodedMaxAmount,
amountPerSecond: decodedAmountPerSecond,
startTime: decodedStartTime,
spent: 0n,
currentTimestamp,
});

return {
availableAmount,
};
};

/**
* Replicates the internal _getAvailableAmount logic from the smart contract.
*
* @param allowance - The allowance object containing all parameters.
* @param allowance.initialAmount - The initial amount available.
* @param allowance.maxAmount - The maximum amount allowed.
* @param allowance.amountPerSecond - The amount streamed per second.
* @param allowance.startTime - The start time of the streaming.
* @param allowance.spent - The amount already spent.
* @param allowance.currentTimestamp - The current timestamp.
* @returns The available amount that can be spent.
*/
function getAvailableAmount(allowance: {
initialAmount: bigint;
maxAmount: bigint;
amountPerSecond: bigint;
startTime: bigint;
spent: bigint;
currentTimestamp: bigint;
}): bigint {
// If current time is before start time, nothing is available
if (allowance.currentTimestamp < allowance.startTime) {
return 0n;
}

// Calculate elapsed time since start
const elapsed = allowance.currentTimestamp - allowance.startTime;

// Calculate total unlocked amount
let unlocked = allowance.initialAmount + allowance.amountPerSecond * elapsed;

// Cap by max amount
if (unlocked > allowance.maxAmount) {
unlocked = allowance.maxAmount;
}

// If spent >= unlocked, nothing available
if (allowance.spent >= unlocked) {
return 0n;
}

// Return available amount
return unlocked - allowance.spent;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { read as getAvailableAmount } from './methods/getAvailableAmount';

export { getAvailableAmount };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as read from './read';

export { read };
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { MultiTokenPeriodEnforcer } from '@metamask/delegation-abis';
import type { Address, Client, Hex } from 'viem';
import { readContract } from 'viem/actions';

export type ReadGetAvailableAmountParameters = {
client: Client;
contractAddress: Address;
delegationHash: Hex;
delegationManager: Address;
terms: Hex;
args: Hex;
};

export const read = async ({
client,
contractAddress,
delegationHash,
delegationManager,
terms,
args,
}: ReadGetAvailableAmountParameters) => {
const [availableAmount, isNewPeriod, currentPeriod] = await readContract(
client,
{
address: contractAddress,
abi: MultiTokenPeriodEnforcer.abi,
functionName: 'getAvailableAmount',
args: [delegationHash, delegationManager, terms, args],
},
);

return {
availableAmount,
isNewPeriod,
currentPeriod,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { read as getAvailableAmount } from './methods/getAvailableAmount';

export { getAvailableAmount };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as read from './read';

export { read };
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { NativeTokenPeriodTransferEnforcer } from '@metamask/delegation-abis';
import type { Address, Client, Hex } from 'viem';
import { readContract } from 'viem/actions';

export type ReadGetAvailableAmountParameters = {
client: Client;
contractAddress: Address;
delegationHash: Hex;
delegationManager: Address;
terms: Hex;
};

export const read = async ({
client,
contractAddress,
delegationHash,
delegationManager,
terms,
}: ReadGetAvailableAmountParameters) => {
const [availableAmount, isNewPeriod, currentPeriod] = await readContract(
client,
{
address: contractAddress,
abi: NativeTokenPeriodTransferEnforcer.abi,
functionName: 'getAvailableAmount',
args: [delegationHash, delegationManager, terms],
},
);

return {
availableAmount,
isNewPeriod,
currentPeriod,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { read as getAvailableAmount } from './methods/getAvailableAmount';

export { getAvailableAmount };
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import * as read from './read';

export { read };
Loading
Loading