@river-build/web3
Version:
Dapps for our Space and Registry contracts
1,330 lines (1,325 loc) • 64 kB
JavaScript
import { ethers } from 'ethers';
import { CheckOperationType, LogicalOperationType, encodeRuleData, decodeRuleData, encodeRuleDataV2, decodeRuleDataV2, NoopOperation, OperationType, evaluateTree, postOrderArrayToTree, treeToRuleData, ruleDataToOperations, encodeThresholdParams, decodeThresholdParams, encodeERC1155Params, decodeERC1155Params, createOperationsTree, EncodedNoopRuleData, DecodedCheckOperationBuilder, evaluateOperationsForEntitledWallet, } from './entitlement';
import { MOCK_ADDRESS, MOCK_ADDRESS_2, MOCK_ADDRESS_3, MOCK_ADDRESS_4, MOCK_ADDRESS_5, } from './Utils';
import { zeroAddress } from 'viem';
import { convertRuleDataV2ToV1 } from './ConvertersEntitlements';
import debug from 'debug';
const log = debug('test');
function makeRandomOperation(depth) {
const rand = Math.random();
if ((depth > 5 && depth < 10 && rand < 1 / 3) || (depth < 10 && rand < 1 / 2)) {
return {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.AND,
leftOperation: makeRandomOperation(depth + 1),
rightOperation: makeRandomOperation(depth + 1),
};
}
else if ((depth > 5 && depth < 10 && rand < 2 / 3) || (depth < 10 && rand > 1 / 2)) {
return {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.OR,
leftOperation: makeRandomOperation(depth + 1),
rightOperation: makeRandomOperation(depth + 1),
};
}
else {
return {
opType: OperationType.CHECK,
checkType: CheckOperationType.MOCK,
chainId: rand > 0.5 ? 1n : 0n,
contractAddress: generateRandomEthAddress(),
params: encodeThresholdParams({ threshold: rand > 0.5 ? 500n : 10n }),
};
}
}
it('random', async () => {
const operation = makeRandomOperation(0);
// it takes a Uint8Array and returns a Uint8Array
const controller = new AbortController();
const result = await evaluateTree(controller, [], xchainConfig, operation);
expect(result).toBeDefined();
});
function generateRandomEthAddress() {
let address = '0x';
const characters = '0123456789abcdef';
for (let i = 0; i < 40; i++) {
address += characters.charAt(Math.floor(Math.random() * characters.length));
}
return address;
}
/**
* An operation that always returns true
*/
const falseCheck = {
opType: OperationType.CHECK,
checkType: CheckOperationType.MOCK,
chainId: 0n,
contractAddress: `0x0`,
params: encodeThresholdParams({ threshold: 10n }),
};
const slowFalseCheck = {
opType: OperationType.CHECK,
checkType: CheckOperationType.MOCK,
chainId: 0n,
contractAddress: '0x1',
params: encodeThresholdParams({ threshold: 500n }),
};
const trueCheck = {
opType: OperationType.CHECK,
checkType: CheckOperationType.MOCK,
chainId: 1n,
contractAddress: '0x0',
params: encodeThresholdParams({ threshold: 10n }),
};
const slowTrueCheck = {
opType: OperationType.CHECK,
checkType: CheckOperationType.MOCK,
chainId: 1n,
contractAddress: '0x1',
params: encodeThresholdParams({ threshold: 500n }),
};
// We have a custom NFT contract deployed to both ethereum sepolia and base sepolia where we
// can mint NFTs for testing. These are included in our unit tests because the local chain
// stack does not always behave the same as remote chains, so our xchain tests use them. We
// reproduce the same unit tests here to ensure parity between evaluation in xchain and the
// client.
// Contract addresses for the test NFT contracts.
const SepoliaTestNftContract = '0xb088b3f2b35511A611bF2aaC13fE605d491D6C19';
const SepoliaTestNftWallet_1Token = '0x1FDBA84c2153568bc22686B88B617CF64cdb0637';
const SepoliaTestNftWallet_3Tokens = '0xB79Af997239A334355F60DBeD75bEDf30AcD37bD';
const SepoliaTestNftWallet_2Tokens = '0x8cECcB1e5537040Fc63A06C88b4c1dE61880dA4d';
const ethereumSepoliaChainId = 11155111n;
const baseSepoliaChainId = 84532n;
const nftCheckEthereumSepolia = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: ethereumSepoliaChainId,
contractAddress: SepoliaTestNftContract,
params: encodeThresholdParams({ threshold: 1n }),
};
const nftMultiCheckEthereumSepolia = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: ethereumSepoliaChainId,
contractAddress: SepoliaTestNftContract,
params: encodeThresholdParams({ threshold: 6n }),
};
const nftMultiCheckHighThresholdEthereumSepolia = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: ethereumSepoliaChainId,
contractAddress: SepoliaTestNftContract,
params: encodeThresholdParams({ threshold: 100n }),
};
const nftCheckBaseSepolia = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: 84532n,
contractAddress: SepoliaTestNftContract,
params: encodeThresholdParams({ threshold: 1n }),
};
const nftMultiCheckBaseSepolia = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: 84532n,
contractAddress: SepoliaTestNftContract,
params: encodeThresholdParams({ threshold: 6n }),
};
const nftMultiCheckHighThresholdBaseSepolia = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: 84532n,
contractAddress: SepoliaTestNftContract,
params: encodeThresholdParams({ threshold: 100n }),
};
const xchainConfig = {
supportedRpcUrls: {
[Number(ethereumSepoliaChainId)]: 'https://ethereum-sepolia-rpc.publicnode.com',
[Number(baseSepoliaChainId)]: 'https://sepolia.base.org',
},
etherBasedChains: [Number(ethereumSepoliaChainId), Number(baseSepoliaChainId)],
};
const minimalEtherChainsConfig = {
supportedRpcUrls: {
[Number(ethereumSepoliaChainId)]: 'https://ethereum-sepolia-rpc.publicnode.com',
[Number(baseSepoliaChainId)]: 'https://sepolia.base.org',
},
etherBasedChains: [Number(ethereumSepoliaChainId)],
};
const nftCases = [
{
desc: 'base sepolia',
check: nftCheckBaseSepolia,
wallets: [SepoliaTestNftWallet_1Token],
expectedResult: true,
},
{
desc: 'base sepolia (no tokens)',
check: nftCheckBaseSepolia,
wallets: [ethers.constants.AddressZero],
expectedResult: false,
},
{
desc: 'base sepolia (insufficient balance)',
check: nftMultiCheckBaseSepolia,
wallets: [SepoliaTestNftWallet_1Token],
expectedResult: false,
},
{
desc: 'base sepolia multi-wallet',
check: nftMultiCheckBaseSepolia,
wallets: [
SepoliaTestNftWallet_1Token,
SepoliaTestNftWallet_2Tokens,
SepoliaTestNftWallet_3Tokens,
],
expectedResult: true,
},
{
desc: 'base sepolia multi-wallet (insufficient balance)',
check: nftMultiCheckHighThresholdBaseSepolia,
wallets: [
SepoliaTestNftWallet_1Token,
SepoliaTestNftWallet_2Tokens,
SepoliaTestNftWallet_3Tokens,
],
expectedResult: false,
},
{
desc: 'eth sepolia',
check: nftCheckEthereumSepolia,
wallets: [SepoliaTestNftWallet_1Token],
expectedResult: true,
},
{
desc: 'eth sepolia (no tokens)',
check: nftCheckEthereumSepolia,
wallets: [ethers.constants.AddressZero],
expectedResult: false,
},
{
desc: 'eth sepolia (insufficient balance)',
check: nftMultiCheckEthereumSepolia,
wallets: [SepoliaTestNftWallet_1Token],
expectedResult: false,
},
{
desc: 'eth sepolia multi-wallet',
check: nftMultiCheckEthereumSepolia,
wallets: [
SepoliaTestNftWallet_1Token,
SepoliaTestNftWallet_2Tokens,
SepoliaTestNftWallet_3Tokens,
],
expectedResult: true,
},
{
desc: 'eth sepolia multi-wallet (insufficient balance)',
check: nftMultiCheckHighThresholdEthereumSepolia,
wallets: [
SepoliaTestNftWallet_1Token,
SepoliaTestNftWallet_2Tokens,
SepoliaTestNftWallet_3Tokens,
],
expectedResult: false,
},
];
it.concurrent.each(nftCases)('erc721Check - $desc', async (props) => {
const { check, wallets, expectedResult } = props;
const controller = new AbortController();
const result = await evaluateTree(controller, wallets, xchainConfig, check);
if (expectedResult) {
expect(result).toBeTruthy();
expect(result).not.toEqual(zeroAddress);
}
else {
expect(result).toEqual(zeroAddress);
}
});
// These are the addresses of the chain link test contract on base sepolia and ethereum sepolia.
const baseSepoliaChainLinkContract = '0xE4aB69C077896252FAFBD49EFD26B5D171A32410';
const ethSepoliaChainLinkContract = '0x779877A7B0D9E8603169DdbD7836e478b4624789';
// The following are the addresses of the wallets that hold the chain link tokens for testing.
// Some wallet addresses are duplicated for the sake of self-documenting variable names.
const sepoliaChainLinkWallet_50Link = '0x4BCfC6962Ab0297aF801da21216014F53B46E991';
const sepoliaChainLinkWallet_25Link = '0xa4D440AeA5F555feEB5AEa0ddcED6e1B9FaD6A9C';
const baseSepoliaChainLinkWallet_25Link2 = '0x4BCfC6962Ab0297aF801da21216014F53B46E991';
const baseSepoliaChainLinkWallet_25Link = '0xa4D440AeA5F555feEB5AEa0ddcED6e1B9FaD6A9C';
const testEmptyAccount = '0xb227905F186095083869928BAb49cA9CE9546817';
// This wallet has .4ETH on Sepolia, and .1ETH on Base Sepolia
const ethWallet_0_5Eth = '0x3ef41b0469c1B808Caad9d643F596023e2aa8f11';
// This wallet has .1ETH on Sepolia, and .1ETH on Base Sepolia
const ethWallet_0_2Eth = '0x4BD04Bf2AAC02238bCcFA75D7bc4Cfd2c019c331';
const chainlinkExp = BigInt(10) ** BigInt(18);
// ERC1155 test contracts and wallets
const baseSepoliaErc1155Contract = '0x60327B4F2936E02B910e8A236d46D0B7C1986DCB';
const baseSepoliaErc1155Wallet_TokenId0_700Tokens = '0x1FDBA84c2153568bc22686B88B617CF64cdb0637';
const baseSepoliaErc1155Wallet_TokenId0_300Tokens = '0xB79Af997239A334355F60DBeD75bEDf30AcD37bD';
const baseSepoliaErc1155Wallet_TokenId1_100Tokens = '0x1FDBA84c2153568bc22686B88B617CF64cdb0637';
const baseSepoliaErc1155Wallet_TokenId1_50Tokens = '0xB79Af997239A334355F60DBeD75bEDf30AcD37bD';
const ethBalance_gt_0_7 = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ETH_BALANCE,
chainId: ethereumSepoliaChainId,
contractAddress: ethers.constants.AddressZero,
params: encodeThresholdParams({ threshold: 700000000000000001n }),
};
const ethBalance_0_7 = {
...ethBalance_gt_0_7,
params: encodeThresholdParams({ threshold: 700000000000000000n }),
};
const ethBalance_0_5 = {
...ethBalance_gt_0_7,
params: encodeThresholdParams({ threshold: 500000000000000000n }),
};
const ethBalance_0_4 = {
...ethBalance_gt_0_7,
params: encodeThresholdParams({ threshold: 400000000000000000n }),
};
const erc20ChainLinkCheckBaseSepolia_20Tokens = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC20,
chainId: 84532n,
contractAddress: baseSepoliaChainLinkContract,
params: encodeThresholdParams({ threshold: 20n * chainlinkExp }),
};
const erc20ChainLinkCheckBaseSepolia_30Tokens = {
...erc20ChainLinkCheckBaseSepolia_20Tokens,
params: encodeThresholdParams({ threshold: 30n * chainlinkExp }),
};
const erc20ChainLinkCheckBaseSepolia_50Tokens = {
...erc20ChainLinkCheckBaseSepolia_20Tokens,
params: encodeThresholdParams({ threshold: 50n * chainlinkExp }),
};
const erc20ChainLinkCheckBaseSepolia_90Tokens = {
...erc20ChainLinkCheckBaseSepolia_20Tokens,
params: encodeThresholdParams({ threshold: 90n * chainlinkExp }),
};
const erc20ChainLinkEthereumSepolia_20Tokens = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC20,
chainId: ethereumSepoliaChainId,
contractAddress: ethSepoliaChainLinkContract,
params: encodeThresholdParams({ threshold: 20n * chainlinkExp }),
};
const erc20ChainLinkCheckEthereumSepolia_30Tokens = {
...erc20ChainLinkEthereumSepolia_20Tokens,
params: encodeThresholdParams({ threshold: 30n * chainlinkExp }),
};
const erc20ChainLinkCheckEthereumSepolia_75Tokens = {
...erc20ChainLinkEthereumSepolia_20Tokens,
params: encodeThresholdParams({ threshold: 75n * chainlinkExp }),
};
const erc20ChainLinkCheckEthereumSepolia_90Tokens = {
...erc20ChainLinkEthereumSepolia_20Tokens,
params: encodeThresholdParams({ threshold: 90n * chainlinkExp }),
};
const erc1155CheckBaseSepolia_TokenId0_700Tokens = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC1155,
chainId: baseSepoliaChainId,
contractAddress: baseSepoliaErc1155Contract,
params: encodeERC1155Params({ threshold: 700n, tokenId: 0n }),
};
const erc1155CheckBaseSepolia_TokenId0_1000Tokens = {
...erc1155CheckBaseSepolia_TokenId0_700Tokens,
params: encodeERC1155Params({ threshold: 1000n, tokenId: 0n }),
};
const erc1155CheckBaseSepolia_TokenId0_1001Tokens = {
...erc1155CheckBaseSepolia_TokenId0_700Tokens,
params: encodeERC1155Params({ threshold: 1001n, tokenId: 0n }),
};
const erc1155CheckBaseSepolia_TokenId1_100Tokens = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC1155,
chainId: baseSepoliaChainId,
contractAddress: baseSepoliaErc1155Contract,
params: encodeERC1155Params({ threshold: 100n, tokenId: 1n }),
};
const erc1155CheckBaseSepolia_TokenId1_150Tokens = {
...erc1155CheckBaseSepolia_TokenId1_100Tokens,
params: encodeERC1155Params({ threshold: 150n, tokenId: 1n }),
};
const erc1155CheckBaseSepolia_TokenId1_151Tokens = {
...erc1155CheckBaseSepolia_TokenId1_100Tokens,
params: encodeERC1155Params({ threshold: 151n, tokenId: 1n }),
};
const erc1155Cases = [
{
desc: 'base sepolia token id 0 (no wallets)',
check: erc1155CheckBaseSepolia_TokenId0_700Tokens,
wallets: [],
expectedResult: false,
},
{
desc: 'base sepolia token id 0 (single wallet, insufficient balance)',
check: erc1155CheckBaseSepolia_TokenId0_700Tokens,
wallets: [baseSepoliaErc1155Wallet_TokenId0_300Tokens],
expectedResult: false,
},
{
desc: 'base sepolia token id 0 (single wallet)',
check: erc1155CheckBaseSepolia_TokenId0_700Tokens,
wallets: [baseSepoliaErc1155Wallet_TokenId0_700Tokens],
expectedResult: true,
},
{
desc: 'base sepolia token id 0 (multiwallet, insufficient balance)',
check: erc1155CheckBaseSepolia_TokenId0_1001Tokens,
wallets: [
baseSepoliaErc1155Wallet_TokenId0_700Tokens,
baseSepoliaErc1155Wallet_TokenId0_300Tokens,
],
expectedResult: false,
},
{
desc: 'base sepolia token id 0 (multiwallet)',
check: erc1155CheckBaseSepolia_TokenId0_1000Tokens,
wallets: [
baseSepoliaErc1155Wallet_TokenId0_700Tokens,
baseSepoliaErc1155Wallet_TokenId0_300Tokens,
],
expectedResult: true,
},
{
desc: 'base sepolia token id 1 (no wallets)',
check: erc1155CheckBaseSepolia_TokenId1_100Tokens,
wallets: [],
expectedResult: false,
},
{
desc: 'base sepolia token id 1 (single wallet, insufficient balance)',
check: erc1155CheckBaseSepolia_TokenId1_100Tokens,
wallets: [baseSepoliaErc1155Wallet_TokenId1_50Tokens],
expectedResult: false,
},
{
desc: 'base sepolia token id 1 (single wallet)',
check: erc1155CheckBaseSepolia_TokenId1_100Tokens,
wallets: [baseSepoliaErc1155Wallet_TokenId1_100Tokens],
expectedResult: true,
},
{
desc: 'base sepolia token id 1 (multiwallet, insufficient balance)',
check: erc1155CheckBaseSepolia_TokenId1_151Tokens,
wallets: [
baseSepoliaErc1155Wallet_TokenId1_100Tokens,
baseSepoliaErc1155Wallet_TokenId1_50Tokens,
],
expectedResult: false,
},
{
desc: 'base sepolia token id 1 (multiwallet)',
check: erc1155CheckBaseSepolia_TokenId1_150Tokens,
wallets: [
baseSepoliaErc1155Wallet_TokenId1_100Tokens,
baseSepoliaErc1155Wallet_TokenId1_50Tokens,
],
expectedResult: true,
},
];
it.concurrent.each(erc1155Cases)('ERC1155 Check - $desc', async (props) => {
const { check, wallets, expectedResult } = props;
const controller = new AbortController();
const result = await evaluateTree(controller, wallets, xchainConfig, check);
if (expectedResult) {
expect(result).not.toEqual(zeroAddress);
}
else {
expect(result).toEqual(zeroAddress);
}
});
const ethBalanceCases = [
{
desc: 'eth balance with no wallet',
check: ethBalance_0_5,
wallets: [],
expectedResult: false,
},
{
desc: 'Eth balance across chains',
check: ethBalance_0_5,
wallets: [ethWallet_0_5Eth],
expectedResult: true,
},
{
desc: 'Eth balance across chains (insufficient balance)',
check: ethBalance_0_5,
wallets: [ethWallet_0_2Eth],
expectedResult: false,
},
{
desc: 'Eth balance across chains (multiwallet)',
check: ethBalance_0_7,
wallets: [ethWallet_0_5Eth, ethWallet_0_2Eth],
expectedResult: true,
},
{
desc: 'Eth balance across chains (multiwallet, insufficient balance)',
check: ethBalance_gt_0_7,
wallets: [ethWallet_0_5Eth, ethWallet_0_2Eth],
expectedResult: false,
},
];
it.concurrent.each(ethBalanceCases)('Eth Balance Check - $desc', async (props) => {
const { check, wallets, expectedResult } = props;
const controller = new AbortController();
const result = await evaluateTree(controller, wallets, xchainConfig, check);
if (expectedResult) {
expect(result).toBeTruthy();
expect(result).not.toEqual(zeroAddress);
}
else {
expect(result).toEqual(zeroAddress);
}
});
const ethBalanceCasesMinimalEtherChains = [
{
desc: 'positive result',
check: ethBalance_0_4,
wallets: [ethWallet_0_5Eth],
expectedResult: true,
},
{
desc: 'negative result',
check: ethBalance_0_5,
wallets: [ethWallet_0_5Eth],
expectedResult: false,
},
];
it.concurrent.each(ethBalanceCasesMinimalEtherChains)('Eth Balance Check - Ether chains < xChain supported chains - $desc', async (props) => {
const { check, wallets, expectedResult } = props;
const controller = new AbortController();
const result = await evaluateTree(controller, wallets, minimalEtherChainsConfig, check);
if (expectedResult) {
expect(result).toBeTruthy();
expect(result).not.toEqual(zeroAddress);
}
else {
expect(result).toEqual(zeroAddress);
}
});
const erc20Cases = [
{
desc: 'base sepolia (empty wallet, false)',
check: erc20ChainLinkCheckBaseSepolia_20Tokens,
wallets: [testEmptyAccount],
expectedResult: false,
},
{
desc: 'base sepolia (single wallet)',
check: erc20ChainLinkCheckBaseSepolia_20Tokens,
wallets: [baseSepoliaChainLinkWallet_25Link],
expectedResult: true,
},
{
desc: 'base sepolia (two wallets)',
check: erc20ChainLinkCheckBaseSepolia_20Tokens,
wallets: [baseSepoliaChainLinkWallet_25Link, testEmptyAccount],
expectedResult: true,
},
{
desc: 'base sepolia (false)',
check: erc20ChainLinkCheckBaseSepolia_30Tokens,
wallets: [baseSepoliaChainLinkWallet_25Link],
expectedResult: false,
},
{
desc: 'base sepolia (two wallets, false)',
check: erc20ChainLinkCheckBaseSepolia_30Tokens,
wallets: [baseSepoliaChainLinkWallet_25Link, testEmptyAccount],
expectedResult: false,
},
{
desc: 'base sepolia (two nonempty wallets, true)',
check: erc20ChainLinkCheckBaseSepolia_30Tokens,
wallets: [baseSepoliaChainLinkWallet_25Link, baseSepoliaChainLinkWallet_25Link2],
expectedResult: true,
},
{
desc: 'base sepolia (two nonempty wallets, exact balance - true)',
check: erc20ChainLinkCheckBaseSepolia_50Tokens,
wallets: [baseSepoliaChainLinkWallet_25Link, baseSepoliaChainLinkWallet_25Link2],
expectedResult: true,
},
{
desc: 'base sepolia (two nonempty wallets, false)',
check: erc20ChainLinkCheckBaseSepolia_90Tokens,
wallets: [baseSepoliaChainLinkWallet_25Link, baseSepoliaChainLinkWallet_25Link2],
expectedResult: false,
},
{
desc: 'eth sepolia (empty wallet, false)',
check: erc20ChainLinkCheckEthereumSepolia_30Tokens,
wallets: [testEmptyAccount],
expectedResult: false,
},
{
desc: 'eth sepolia (single wallet)',
check: erc20ChainLinkEthereumSepolia_20Tokens,
wallets: [sepoliaChainLinkWallet_25Link],
expectedResult: true,
},
{
desc: 'eth sepolia (two wallets)',
check: erc20ChainLinkEthereumSepolia_20Tokens,
wallets: [sepoliaChainLinkWallet_25Link, testEmptyAccount],
expectedResult: true,
},
{
desc: 'eth sepolia (false)',
check: erc20ChainLinkCheckBaseSepolia_30Tokens,
wallets: [sepoliaChainLinkWallet_25Link],
expectedResult: false,
},
{
desc: 'eth sepolia (two wallets, false)',
check: erc20ChainLinkCheckBaseSepolia_30Tokens,
wallets: [sepoliaChainLinkWallet_25Link, testEmptyAccount],
expectedResult: false,
},
{
desc: 'eth sepolia (two nonempty wallets, exact balance - true)',
check: erc20ChainLinkCheckEthereumSepolia_75Tokens,
wallets: [sepoliaChainLinkWallet_25Link, sepoliaChainLinkWallet_50Link],
expectedResult: true,
},
{
desc: 'eth sepolia (two nonempty wallets, false)',
check: erc20ChainLinkCheckEthereumSepolia_90Tokens,
wallets: [sepoliaChainLinkWallet_25Link, sepoliaChainLinkWallet_50Link],
expectedResult: false,
},
];
it.concurrent.each(erc20Cases)('erc20Check - $desc', async (props) => {
const { check, wallets, expectedResult } = props;
const controller = new AbortController();
const result = await evaluateTree(controller, wallets, xchainConfig, check);
if (expectedResult) {
expect(result).toBeTruthy();
expect(result).not.toEqual(zeroAddress);
}
else {
expect(result).toEqual(zeroAddress);
}
});
const errorTests = [
{
desc: 'unknown check type',
check: {
...erc20ChainLinkCheckBaseSepolia_20Tokens,
checkType: CheckOperationType.NONE,
},
error: 'Unknown check operation type',
},
{
desc: 'erc20 invalid check (chainId)',
check: {
...erc20ChainLinkCheckBaseSepolia_20Tokens,
chainId: -1n,
},
error: 'Invalid chain id for check operation ERC20',
},
{
desc: 'erc20 invalid check (contractAddress)',
check: {
...erc20ChainLinkCheckBaseSepolia_20Tokens,
contractAddress: ethers.constants.AddressZero,
},
error: 'Invalid contract address for check operation ERC20',
},
{
desc: 'erc20 invalid check (threshold)',
check: {
...erc20ChainLinkCheckBaseSepolia_20Tokens,
params: encodeThresholdParams({ threshold: 0n }),
},
error: 'Invalid threshold for check operation ERC20',
},
{
desc: 'erc721 invalid check (chainId)',
check: {
...nftCheckBaseSepolia,
opType: OperationType.CHECK,
chainId: -1n,
},
error: 'Invalid chain id for check operation ERC721',
},
{
desc: 'erc721 invalid check (contractAddress)',
check: {
...nftCheckBaseSepolia,
opType: OperationType.CHECK,
contractAddress: ethers.constants.AddressZero,
},
error: 'Invalid contract address for check operation ERC721',
},
{
desc: 'erc721 invalid check (threshold)',
check: {
...nftCheckBaseSepolia,
opType: OperationType.CHECK,
params: encodeThresholdParams({ threshold: 0n }),
},
error: 'Invalid threshold for check operation ERC721',
},
{
desc: 'cross chain entitlement invalid check (chainId)',
check: {
opType: OperationType.CHECK,
checkType: CheckOperationType.ISENTITLED,
chainId: -1n,
contractAddress: nftCheckBaseSepolia.contractAddress,
},
error: 'Invalid chain id for check operation ISENTITLED',
},
{
desc: 'cross chain entitlement invalid check (contractAddress)',
check: {
opType: OperationType.CHECK,
checkType: CheckOperationType.ISENTITLED,
chainId: 1n,
contractAddress: ethers.constants.AddressZero,
},
error: 'Invalid contract address for check operation ISENTITLED',
},
{
desc: 'erc1155 invalid check (chainId)',
check: {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC1155,
chainId: -1n,
contractAddress: MOCK_ADDRESS,
params: encodeERC1155Params({ tokenId: 1n, threshold: 1n }),
},
error: 'Invalid chain id for check operation ERC1155',
},
{
desc: 'erc 1155 invalid check (contractAddress)',
check: {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC1155,
chainId: 1n,
contractAddress: ethers.constants.AddressZero,
},
error: 'Invalid contract address for check operation ERC1155',
},
{
desc: 'erc1155 invalid check (threshold)',
check: {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC1155,
chainId: 1n,
contractAddress: MOCK_ADDRESS,
params: encodeERC1155Params({ tokenId: 1n, threshold: 0n }),
},
error: 'Invalid threshold for check operation ERC1155',
},
{
desc: 'eth balance invalid check (invalid threshold: 0)',
check: {
opType: OperationType.CHECK,
checkType: CheckOperationType.ETH_BALANCE,
chainId: 0n,
contractAddress: zeroAddress,
params: encodeThresholdParams({ threshold: 0n }),
},
error: 'Invalid threshold for check operation ETH_BALANCE',
},
];
it.concurrent.each(errorTests)('error - $desc', async (props) => {
const { check, error } = props;
const controller = new AbortController();
await expect(evaluateTree(controller, [SepoliaTestNftWallet_1Token], xchainConfig, check)).rejects.toThrow(error);
});
const orCases = [
{ leftCheck: trueCheck, rightCheck: trueCheck, expectedResult: MOCK_ADDRESS },
{ leftCheck: trueCheck, rightCheck: falseCheck, expectedResult: MOCK_ADDRESS },
{ leftCheck: falseCheck, rightCheck: trueCheck, expectedResult: MOCK_ADDRESS },
{ leftCheck: falseCheck, rightCheck: falseCheck, expectedResult: ethers.constants.AddressZero },
];
it.concurrent.each(orCases)('orOperation', async (props) => {
const { leftCheck, rightCheck, expectedResult } = props;
const orOperation = {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.OR,
leftOperation: leftCheck,
rightOperation: rightCheck,
};
const controller = new AbortController();
const result = await evaluateTree(controller, [], xchainConfig, orOperation);
expect(result).toBe(expectedResult);
});
const slowOrCases = [
{
leftCheck: trueCheck,
rightCheck: slowTrueCheck,
expectedResult: MOCK_ADDRESS,
expectedTime: 10,
},
{
leftCheck: trueCheck,
rightCheck: slowFalseCheck,
expectedResult: MOCK_ADDRESS,
expectedTime: 10,
},
{
leftCheck: slowFalseCheck,
rightCheck: trueCheck,
expectedResult: MOCK_ADDRESS,
expectedTime: 10,
},
{
leftCheck: falseCheck,
rightCheck: slowFalseCheck,
expectedResult: ethers.constants.AddressZero,
expectedTime: 500,
},
];
it.concurrent.each(slowOrCases)('slowOrOperation', async (props) => {
const { leftCheck, rightCheck, expectedResult, expectedTime } = props;
const operation = {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.OR,
leftOperation: leftCheck,
rightOperation: rightCheck,
};
const controller = new AbortController();
const start = performance.now();
const result = await evaluateTree(controller, [], xchainConfig, operation);
const timeTaken = performance.now() - start;
expect(timeTaken).toBeCloseTo(expectedTime, -2);
expect(result).toBe(expectedResult);
});
const andCases = [
{ leftCheck: trueCheck, rightCheck: trueCheck, expectedResult: MOCK_ADDRESS },
{ leftCheck: trueCheck, rightCheck: falseCheck, expectedResult: ethers.constants.AddressZero },
{ leftCheck: falseCheck, rightCheck: trueCheck, expectedResult: ethers.constants.AddressZero },
{ leftCheck: falseCheck, rightCheck: falseCheck, expectedResult: ethers.constants.AddressZero },
];
it.concurrent.each(andCases)('andOperation', async (props) => {
const { leftCheck, rightCheck, expectedResult } = props;
const operation = {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.AND,
leftOperation: leftCheck,
rightOperation: rightCheck,
};
const controller = new AbortController();
const result = await evaluateTree(controller, [], xchainConfig, operation);
expect(result).toBe(expectedResult);
});
const slowAndCases = [
{
leftCheck: trueCheck,
rightCheck: slowTrueCheck,
expectedResult: MOCK_ADDRESS,
expectedTime: 500,
},
{
leftCheck: slowTrueCheck,
rightCheck: falseCheck,
expectedResult: ethers.constants.AddressZero,
expectedTime: 10,
},
{
leftCheck: falseCheck,
rightCheck: slowTrueCheck,
expectedResult: ethers.constants.AddressZero,
expectedTime: 10,
},
{
leftCheck: falseCheck,
rightCheck: slowFalseCheck,
expectedResult: ethers.constants.AddressZero,
expectedTime: 10,
},
];
it.concurrent.each(slowAndCases)('slowAndOperation', async (props) => {
const { leftCheck, rightCheck, expectedResult, expectedTime } = props;
const operation = {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.AND,
leftOperation: leftCheck,
rightOperation: rightCheck,
};
const controller = new AbortController();
const start = performance.now();
const result = await evaluateTree(controller, [], xchainConfig, operation);
const timeTaken = performance.now() - start;
expect(result).toBe(expectedResult);
expect(timeTaken).toBeCloseTo(expectedTime, -2);
});
it('empty', async () => {
const controller = new AbortController();
const result = await evaluateTree(controller, [], xchainConfig, undefined);
expect(result).toBe(ethers.constants.AddressZero);
});
it('true', async () => {
const operation = trueCheck;
const controller = new AbortController();
const result = await evaluateTree(controller, [], xchainConfig, operation);
expect(result).toBe(MOCK_ADDRESS);
});
it('false', async () => {
const operation = falseCheck;
const controller = new AbortController();
const result = await evaluateTree(controller, [], xchainConfig, operation);
expect(result).toBe(ethers.constants.AddressZero);
});
it('encode/decode rule data v2', async () => {
const randomTree = makeRandomOperation(5);
const data = treeToRuleData(randomTree);
const encoded = encodeRuleDataV2(data);
const decodedDag = decodeRuleDataV2(encoded);
const operations = ruleDataToOperations(decodedDag);
const newTree = postOrderArrayToTree(operations);
expect(randomTree.opType === newTree.opType).toBeTruthy();
});
it('decode empty ruledata v2 to NoopRuleData v1', async () => {
const converted = convertRuleDataV2ToV1(decodeRuleDataV2(EncodedNoopRuleData));
expect(converted.operations).toHaveLength(0);
expect(converted.checkOperations).toHaveLength(0);
expect(converted.logicalOperations).toHaveLength(0);
});
// encode/decode should respect address equality semantics but may not maintain case
function addressesEqual(a, b) {
return a.toLowerCase() === b.toLowerCase();
}
it('encode/decode rule data', async () => {
const randomTree = makeRandomOperation(5);
const data = treeToRuleData(randomTree);
const v1 = convertRuleDataV2ToV1(data);
const encoded = encodeRuleData(v1);
const decodedDag = decodeRuleData(encoded);
for (let i = 0; i < v1.operations.length; i++) {
expect(v1.operations[i].opType).toBe(decodedDag.operations[i].opType);
expect(v1.operations[i].index).toBe(decodedDag.operations[i].index);
}
for (let i = 0; i < v1.logicalOperations.length; i++) {
expect(v1.logicalOperations[i].logOpType).toBe(decodedDag.logicalOperations[i].logOpType);
expect(v1.logicalOperations[i].leftOperationIndex).toBe(decodedDag.logicalOperations[i].leftOperationIndex);
expect(v1.logicalOperations[i].rightOperationIndex).toBe(decodedDag.logicalOperations[i].rightOperationIndex);
}
for (let i = 0; i < v1.checkOperations.length; i++) {
expect(v1.checkOperations[i].opType).toBe(decodedDag.checkOperations[i].opType);
expect(v1.checkOperations[i].chainId).toBe(decodedDag.checkOperations[i].chainId);
expect(addressesEqual(v1.checkOperations[i].contractAddress, decodedDag.checkOperations[i].contractAddress)).toBeTruthy();
expect(v1.checkOperations[i].threshold).toBe(decodedDag.checkOperations[i].threshold);
}
});
describe.concurrent('threshold params', () => {
it('encode/decode', () => {
const encodedParams = encodeThresholdParams({ threshold: BigInt(100) });
const decodedParams = decodeThresholdParams(encodedParams);
expect(decodedParams).toEqual({ threshold: BigInt(100) });
});
it('encode invalid params', () => {
expect(() => encodeThresholdParams({ threshold: BigInt(-1) })).toThrow('Invalid threshold -1: must be greater than or equal to 0');
});
});
describe.concurrent('erc1155 params', () => {
it('encode invalid params', () => {
expect(() => encodeERC1155Params({ threshold: BigInt(-1), tokenId: BigInt(100) })).toThrow('Invalid threshold -1: must be greater than or equal to 0');
});
it('encode invalid token id', () => {
expect(() => encodeERC1155Params({ threshold: BigInt(100), tokenId: BigInt(-1) })).toThrow('Invalid tokenId -1: must be greater than or equal to 0');
});
it('encode/decode', () => {
const encodedParams = encodeERC1155Params({ threshold: BigInt(200), tokenId: BigInt(100) });
const decodedParams = decodeERC1155Params(encodedParams);
expect(decodedParams).toEqual({ threshold: BigInt(200), tokenId: BigInt(100) });
});
});
function assertRuleDatasEqual(actual, expected) {
expect(expected.operations.length).toBe(actual.operations.length);
for (let i = 0; i < expected.operations.length; i++) {
expect(expected.operations[i].opType).toBe(actual.operations[i].opType);
expect(expected.operations[i].index).toBe(actual.operations[i].index);
}
expect(expected.checkOperations.length).toBe(actual.checkOperations.length);
for (let i = 0; i < expected.checkOperations.length; i++) {
expect(expected.checkOperations[i].opType).toBe(actual.checkOperations[i].opType);
expect(expected.checkOperations[i].chainId).toBe(actual.checkOperations[i].chainId);
expect(expected.checkOperations[i].contractAddress).toBe(actual.checkOperations[i].contractAddress);
expect(expected.checkOperations[i].params).toBe(actual.checkOperations[i].params);
}
expect(expected.logicalOperations.length).toBe(actual.logicalOperations.length);
for (let i = 0; i < expected.logicalOperations.length; i++) {
expect(expected.logicalOperations[i].logOpType).toBe(actual.logicalOperations[i].logOpType);
expect(expected.logicalOperations[i].leftOperationIndex).toBe(actual.logicalOperations[i].leftOperationIndex);
}
}
function assertOperationEqual(actual, expected) {
expect(actual.opType).toBe(expected.opType);
if (expected.opType === OperationType.CHECK) {
const actualCheck = actual;
const expectedCheck = expected;
expect(actualCheck.checkType).toBe(expectedCheck.checkType);
expect(actualCheck.chainId).toBe(expectedCheck.chainId);
expect(actualCheck.contractAddress).toBe(expectedCheck.contractAddress);
expect(actualCheck.params).toBe(expectedCheck.params);
}
else if (expected.opType === OperationType.LOGICAL) {
const actualLogical = actual;
const expectedLogical = expected;
expect(actualLogical.logicalType).toBe(expectedLogical.logicalType);
// This check involves some redundance since these element have been visited already,
// but it ensures that embedded operations in the tree are equal since the
// operations tree does not use indices, but builds a tree directly.
assertOperationEqual(actualLogical.leftOperation, expectedLogical.leftOperation);
assertOperationEqual(actualLogical.rightOperation, expectedLogical.rightOperation);
}
else if (expected.opType === OperationType.NONE) {
expect(actual.opType).toBe(expected.opType);
}
}
function assertOperationsEqual(actual, expected) {
expect(expected.length).toBe(actual.length);
for (let i = 0; i < expected.length; i++) {
assertOperationEqual(actual[i], expected[i]);
}
}
describe.concurrent('createOperationsTree', () => {
it('empty', () => {
const checkOp = [];
const tree = createOperationsTree(checkOp);
expect(tree).toEqual({
operations: [NoopOperation],
checkOperations: [],
logicalOperations: [],
});
// Validate conversion of rule data to operations tree (used for evaluation)
const operations = ruleDataToOperations(tree);
assertOperationsEqual(operations, [NoopOperation]);
});
it('custom entitlement check', () => {
const checkOp = [
{
type: CheckOperationType.ISENTITLED,
chainId: 1234n,
address: MOCK_ADDRESS,
byteEncodedParams: `0xdeadbeefdeadbeef12341234`,
},
];
const tree = createOperationsTree(checkOp);
// Validate the constructed rule data
assertRuleDatasEqual(tree, {
operations: [
{
opType: OperationType.CHECK,
index: 0,
},
],
checkOperations: [
{
opType: CheckOperationType.ISENTITLED,
chainId: 1234n,
contractAddress: MOCK_ADDRESS,
params: `0xdeadbeefdeadbeef12341234`,
},
],
logicalOperations: [],
});
});
it('single check', () => {
const checkOp = [
{
type: CheckOperationType.ERC721,
chainId: 1n,
address: MOCK_ADDRESS,
threshold: BigInt(1),
},
];
const tree = createOperationsTree(checkOp);
// Validate the constructed rule data
assertRuleDatasEqual(tree, {
operations: [
{
opType: OperationType.CHECK,
index: 0,
},
],
checkOperations: [
{
opType: CheckOperationType.ERC721,
chainId: 1n,
contractAddress: MOCK_ADDRESS,
params: encodeThresholdParams({ threshold: BigInt(1) }),
},
],
logicalOperations: [],
});
});
it('two checks', () => {
const checkOp = [
{
type: CheckOperationType.ISENTITLED,
chainId: 1n,
address: MOCK_ADDRESS,
},
{
type: CheckOperationType.ERC721,
chainId: 1n,
address: MOCK_ADDRESS_2,
threshold: BigInt(1),
},
];
const tree = createOperationsTree(checkOp);
// Validate the constructed rule data
assertRuleDatasEqual(tree, {
operations: [
{
opType: OperationType.CHECK,
index: 0,
},
{
opType: OperationType.CHECK,
index: 1,
},
{
opType: OperationType.LOGICAL,
index: 0,
},
],
checkOperations: [
{
opType: CheckOperationType.ISENTITLED,
chainId: 1n,
contractAddress: MOCK_ADDRESS,
params: '0x',
},
{
opType: CheckOperationType.ERC721,
chainId: 1n,
contractAddress: MOCK_ADDRESS_2,
params: encodeThresholdParams({ threshold: BigInt(1) }),
},
],
logicalOperations: [
{
logOpType: LogicalOperationType.OR,
leftOperationIndex: 0,
rightOperationIndex: 1,
},
],
});
// Validate conversion of rule data to operations tree (used for evaluation)
const operations = ruleDataToOperations(tree);
const check1 = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ISENTITLED,
chainId: 1n,
contractAddress: MOCK_ADDRESS,
params: '0x',
};
const check2 = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: 1n,
contractAddress: MOCK_ADDRESS_2,
params: encodeThresholdParams({ threshold: BigInt(1) }),
};
assertOperationsEqual(operations, [
check1,
check2,
{
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.OR,
leftOperation: check1,
rightOperation: check2,
},
]);
});
it('three checks', () => {
/*
3-check tree:
============
logical2
--------
/ \
logical1 check3
--------
/ \
check1 check2
Postorder: check1, check2, logical1, check3, logical2
*/
const checkOp = [
{
type: CheckOperationType.ISENTITLED,
chainId: 1n,
address: MOCK_ADDRESS,
byteEncodedParams: `0xabcdef`,
},
{
type: CheckOperationType.ERC721,
chainId: 2n,
address: MOCK_ADDRESS_2,
threshold: BigInt(1),
},
{
type: CheckOperationType.ERC20,
chainId: 3n,
address: MOCK_ADDRESS_3,
threshold: BigInt(1),
},
];
const tree = createOperationsTree(checkOp);
// Validate the constructed rule data
assertRuleDatasEqual(tree, {
operations: [
{
opType: OperationType.CHECK,
index: 0,
},
{
opType: OperationType.CHECK,
index: 1,
},
{
opType: OperationType.LOGICAL,
index: 0,
},
{
opType: OperationType.CHECK,
index: 2,
},
{
opType: OperationType.LOGICAL,
index: 1,
},
],
checkOperations: [
{
opType: CheckOperationType.ISENTITLED,
chainId: 1n,
contractAddress: MOCK_ADDRESS,
params: '0xabcdef',
},
{
opType: CheckOperationType.ERC721,
chainId: 2n,
contractAddress: MOCK_ADDRESS_2,
params: encodeThresholdParams({ threshold: BigInt(1) }),
},
{
opType: CheckOperationType.ERC20,
chainId: 3n,
contractAddress: MOCK_ADDRESS_3,
params: encodeThresholdParams({ threshold: BigInt(1) }),
},
],
logicalOperations: [
{
logOpType: LogicalOperationType.OR,
leftOperationIndex: 0,
rightOperationIndex: 1,
},
{
logOpType: LogicalOperationType.OR,
leftOperationIndex: 2,
rightOperationIndex: 3,
},
],
});
// Validate conversion of rule data to operations tree (used for evaluation)
const operations = ruleDataToOperations(tree);
const check1 = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ISENTITLED,
chainId: 1n,
contractAddress: MOCK_ADDRESS,
params: '0xabcdef',
};
const check2 = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC721,
chainId: 2n,
contractAddress: MOCK_ADDRESS_2,
params: encodeThresholdParams({ threshold: BigInt(1) }),
};
const check3 = {
opType: OperationType.CHECK,
checkType: CheckOperationType.ERC20,
chainId: 3n,
contractAddress: MOCK_ADDRESS_3,
params: encodeThresholdParams({ threshold: BigInt(1) }),
};
const logical1 = {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.OR,
leftOperation: check1,
rightOperation: check2,
};
const logical2 = {
opType: OperationType.LOGICAL,
logicalType: LogicalOperationType.OR,
leftOperation: logical1,
rightOperation: check3,
};
assertOperationsEqual(operations, [check1, check2, logical1, check3, logical2]);
});
it('five checks', () => {
/*
5-check tree:
=============
logical4
--------
/ \
logical3 check5
--------
/ \
logical1 logical2
-------- --------
/ \ / \
check1 check2 check3 check4
Postorder: check1, check2, logical1, check3, check4, logical2, logical3, check5, logical4
*/
const checkOp = [
{
type: CheckOperationType.ISENTITLED,
chainId: 1n,
address: MOCK_ADDRESS,
byteEncodedParams: `0xabcdef`,
},
{
type: CheckOperationType.ERC721,
chainId: 2n,
address: MOCK_ADDRESS_2,
threshold: BigInt(2),
},
{
type: CheckOperationType.ERC20,
chainId: 3n,
address: MOCK_ADDRESS_3,
threshold: BigInt(3),
},
{
type: CheckOperationType.ERC721,
chainId: 4n,
address: MOCK_ADDRESS_4,
threshold: BigInt(4),
},
{
type: CheckOperationType.ERC20,
chainId: 5n,
address: MOCK_ADDRESS_5,
threshold: BigInt(5),
},
];
const tree = createOperationsTree(checkOp);
// Validate the constructed rule data
log('tree', tree);
const expectedTree = {
operations: [
{
opType: OperationType.CHECK,
index: 0,
},
{
opType: OperationType.CHECK,
index: 1,
},
{
opType: OperationType.LOGICAL,