diff --git a/contracts/test/mocks/MockCustomEntitlement.sol b/contracts/test/mocks/MockCustomEntitlement.sol index 23c09f77a..e57cc93cc 100644 --- a/contracts/test/mocks/MockCustomEntitlement.sol +++ b/contracts/test/mocks/MockCustomEntitlement.sol @@ -8,13 +8,20 @@ contract MockCustomEntitlement is ICustomEntitlement { constructor() {} - function setEntitled(address[] memory user, bool userIsEntitled) external { - entitled[keccak256(abi.encode(user))] = userIsEntitled; + function setEntitled(address[] memory users, bool userIsEntitled) external { + for (uint256 i = 0; i < users.length; i++) { + entitled[keccak256(abi.encode(users[i]))] = userIsEntitled; + } } function isEntitled( - address[] memory user + address[] memory users ) external view override returns (bool) { - return entitled[keccak256(abi.encode(user))]; + for (uint256 i = 0; i < users.length; i++) { + if (entitled[keccak256(abi.encode(users[i]))] == true) { + return true; + } + } + return false; } } diff --git a/packages/generated/dev/abis/MockCustomEntitlement.abi.json b/packages/generated/dev/abis/MockCustomEntitlement.abi.json index bf21b011f..c6bd15a15 100644 --- a/packages/generated/dev/abis/MockCustomEntitlement.abi.json +++ b/packages/generated/dev/abis/MockCustomEntitlement.abi.json @@ -9,7 +9,7 @@ "name": "isEntitled", "inputs": [ { - "name": "user", + "name": "users", "type": "address[]", "internalType": "address[]" } @@ -28,7 +28,7 @@ "name": "setEntitled", "inputs": [ { - "name": "user", + "name": "users", "type": "address[]", "internalType": "address[]" }, diff --git a/packages/generated/dev/abis/MockCustomEntitlement.abi.ts b/packages/generated/dev/abis/MockCustomEntitlement.abi.ts index 69711dacb..1ed8b3dba 100644 --- a/packages/generated/dev/abis/MockCustomEntitlement.abi.ts +++ b/packages/generated/dev/abis/MockCustomEntitlement.abi.ts @@ -9,7 +9,7 @@ export default [ "name": "isEntitled", "inputs": [ { - "name": "user", + "name": "users", "type": "address[]", "internalType": "address[]" } @@ -28,7 +28,7 @@ export default [ "name": "setEntitled", "inputs": [ { - "name": "user", + "name": "users", "type": "address[]", "internalType": "address[]" }, diff --git a/packages/generated/dev/abis/MockCustomEntitlement.bin b/packages/generated/dev/abis/MockCustomEntitlement.bin index cfd3d50d1..2272420bb 100644 --- a/packages/generated/dev/abis/MockCustomEntitlement.bin +++ b/packages/generated/dev/abis/MockCustomEntitlement.bin @@ -1 +1 @@ -608060405234801561001057600080fd5b506102c1806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633f4c4d831461003b578063ddc6e68e14610050575b600080fd5b61004e6100493660046101e0565b610077565b005b61006361005e366004610237565b6100c4565b604051901515815260200160405180910390f35b806000808460405160200161008c9190610274565b60408051808303601f19018152918152815160209283012083529082019290925201600020805460ff19169115159190911790555050565b6000806000836040516020016100da9190610274565b60408051601f198184030181529181528151602092830120835290820192909252016000205460ff1692915050565b634e487b7160e01b600052604160045260246000fd5b80356001600160a01b038116811461013657600080fd5b919050565b600082601f83011261014c57600080fd5b8135602067ffffffffffffffff8083111561016957610169610109565b8260051b604051601f19603f8301168101818110848211171561018e5761018e610109565b60405293845260208187018101949081019250878511156101ae57600080fd5b6020870191505b848210156101d5576101c68261011f565b835291830191908301906101b5565b979650505050505050565b600080604083850312156101f357600080fd5b823567ffffffffffffffff81111561020a57600080fd5b6102168582860161013b565b9250506020830135801515811461022c57600080fd5b809150509250929050565b60006020828403121561024957600080fd5b813567ffffffffffffffff81111561026057600080fd5b61026c8482850161013b565b949350505050565b6020808252825182820181905260009190848201906040850190845b818110156102b55783516001600160a01b031683529284019291840191600101610290565b5090969550505050505056 \ No newline at end of file +608060405234801561001057600080fd5b50610317806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633f4c4d831461003b578063ddc6e68e14610050575b600080fd5b61004e61004936600461026d565b610077565b005b61006361005e3660046102c4565b610101565b604051901515815260200160405180910390f35b60005b82518110156100fc578160008085848151811061009957610099610301565b60200260200101516040516020016100c091906001600160a01b0391909116815260200190565b60408051808303601f19018152918152815160209283012083529082019290925201600020805460ff191691151591909117905560010161007a565b505050565b6000805b825181101561018d5760008084838151811061012357610123610301565b602002602001015160405160200161014a91906001600160a01b0391909116815260200190565b60408051601f198184030181529181528151602092830120835290820192909252016000205460ff1615156001036101855750600192915050565b600101610105565b50600092915050565b634e487b7160e01b600052604160045260246000fd5b80356001600160a01b03811681146101c357600080fd5b919050565b600082601f8301126101d957600080fd5b8135602067ffffffffffffffff808311156101f6576101f6610196565b8260051b604051601f19603f8301168101818110848211171561021b5761021b610196565b604052938452602081870181019490810192508785111561023b57600080fd5b6020870191505b8482101561026257610253826101ac565b83529183019190830190610242565b979650505050505050565b6000806040838503121561028057600080fd5b823567ffffffffffffffff81111561029757600080fd5b6102a3858286016101c8565b925050602083013580151581146102b957600080fd5b809150509250929050565b6000602082840312156102d657600080fd5b813567ffffffffffffffff8111156102ed57600080fd5b6102f9848285016101c8565b949350505050565b634e487b7160e01b600052603260045260246000fd \ No newline at end of file diff --git a/packages/generated/dev/abis/MockCustomEntitlement.json b/packages/generated/dev/abis/MockCustomEntitlement.json index 56e34cd9e..7b526102a 100644 --- a/packages/generated/dev/abis/MockCustomEntitlement.json +++ b/packages/generated/dev/abis/MockCustomEntitlement.json @@ -1 +1 @@ -{"abi":[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isEntitled","inputs":[{"name":"user","type":"address[]","internalType":"address[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"setEntitled","inputs":[{"name":"user","type":"address[]","internalType":"address[]"},{"name":"userIsEntitled","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"}],"bytecode":{"object":"0x608060405234801561001057600080fd5b506102c1806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633f4c4d831461003b578063ddc6e68e14610050575b600080fd5b61004e6100493660046101e0565b610077565b005b61006361005e366004610237565b6100c4565b604051901515815260200160405180910390f35b806000808460405160200161008c9190610274565b60408051808303601f19018152918152815160209283012083529082019290925201600020805460ff19169115159190911790555050565b6000806000836040516020016100da9190610274565b60408051601f198184030181529181528151602092830120835290820192909252016000205460ff1692915050565b634e487b7160e01b600052604160045260246000fd5b80356001600160a01b038116811461013657600080fd5b919050565b600082601f83011261014c57600080fd5b8135602067ffffffffffffffff8083111561016957610169610109565b8260051b604051601f19603f8301168101818110848211171561018e5761018e610109565b60405293845260208187018101949081019250878511156101ae57600080fd5b6020870191505b848210156101d5576101c68261011f565b835291830191908301906101b5565b979650505050505050565b600080604083850312156101f357600080fd5b823567ffffffffffffffff81111561020a57600080fd5b6102168582860161013b565b9250506020830135801515811461022c57600080fd5b809150509250929050565b60006020828403121561024957600080fd5b813567ffffffffffffffff81111561026057600080fd5b61026c8482850161013b565b949350505050565b6020808252825182820181905260009190848201906040850190845b818110156102b55783516001600160a01b031683529284019291840191600101610290565b5090969550505050505056","sourceMap":"152:404:356:-:0;;;247:16;;;;;;;;;;152:404;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x608060405234801561001057600080fd5b50600436106100365760003560e01c80633f4c4d831461003b578063ddc6e68e14610050575b600080fd5b61004e6100493660046101e0565b610077565b005b61006361005e366004610237565b6100c4565b604051901515815260200160405180910390f35b806000808460405160200161008c9190610274565b60408051808303601f19018152918152815160209283012083529082019290925201600020805460ff19169115159190911790555050565b6000806000836040516020016100da9190610274565b60408051601f198184030181529181528151602092830120835290820192909252016000205460ff1692915050565b634e487b7160e01b600052604160045260246000fd5b80356001600160a01b038116811461013657600080fd5b919050565b600082601f83011261014c57600080fd5b8135602067ffffffffffffffff8083111561016957610169610109565b8260051b604051601f19603f8301168101818110848211171561018e5761018e610109565b60405293845260208187018101949081019250878511156101ae57600080fd5b6020870191505b848210156101d5576101c68261011f565b835291830191908301906101b5565b979650505050505050565b600080604083850312156101f357600080fd5b823567ffffffffffffffff81111561020a57600080fd5b6102168582860161013b565b9250506020830135801515811461022c57600080fd5b809150509250929050565b60006020828403121561024957600080fd5b813567ffffffffffffffff81111561026057600080fd5b61026c8482850161013b565b949350505050565b6020808252825182820181905260009190848201906040850190845b818110156102b55783516001600160a01b031683529284019291840191600101610290565b5090969550505050505056","sourceMap":"152:404:356:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;267:139;;;;;;:::i;:::-;;:::i;:::-;;410:144;;;;;;:::i;:::-;;:::i;:::-;;;2275:14:542;;2268:22;2250:41;;2238:2;2223:18;410:144:356;;;;;;;267:139;387:14;347:8;:37;377:4;366:16;;;;;;;;:::i;:::-;;;;;;;-1:-1:-1;;366:16:356;;;;;;356:27;;366:16;356:27;;;;347:37;;;;;;;;;;-1:-1:-1;347:37:356;:54;;-1:-1:-1;;347:54:356;;;;;;;;;;-1:-1:-1;;267:139:356:o;410:144::-;493:4;512:8;:37;542:4;531:16;;;;;;;;:::i;:::-;;;;-1:-1:-1;;531:16:356;;;;;;;;;521:27;;531:16;521:27;;;;512:37;;;;;;;;;;-1:-1:-1;512:37:356;;;;;410:144;-1:-1:-1;;410:144:356:o;14:127:542:-;75:10;70:3;66:20;63:1;56:31;106:4;103:1;96:15;130:4;127:1;120:15;146:173;214:20;;-1:-1:-1;;;;;263:31:542;;253:42;;243:70;;309:1;306;299:12;243:70;146:173;;;:::o;324:914::-;378:5;431:3;424:4;416:6;412:17;408:27;398:55;;449:1;446;439:12;398:55;485:6;472:20;511:4;534:18;571:2;567;564:10;561:36;;;577:18;;:::i;:::-;623:2;620:1;616:10;655:2;649:9;718:2;714:7;709:2;705;701:11;697:25;689:6;685:38;773:6;761:10;758:22;753:2;741:10;738:18;735:46;732:72;;;784:18;;:::i;:::-;820:2;813:22;870:18;;;916:4;948:15;;;944:26;;;904:17;;;;-1:-1:-1;982:15:542;;;979:35;;;1010:1;1007;1000:12;979:35;1046:4;1038:6;1034:17;1023:28;;1060:148;1076:6;1071:3;1068:15;1060:148;;;1142:23;1161:3;1142:23;:::i;:::-;1130:36;;1186:12;;;;1093;;;;1060:148;;;1226:6;324:914;-1:-1:-1;;;;;;;324:914:542:o;1243:509::-;1333:6;1341;1394:2;1382:9;1373:7;1369:23;1365:32;1362:52;;;1410:1;1407;1400:12;1362:52;1450:9;1437:23;1483:18;1475:6;1472:30;1469:50;;;1515:1;1512;1505:12;1469:50;1538:61;1591:7;1582:6;1571:9;1567:22;1538:61;:::i;:::-;1528:71;;;1649:2;1638:9;1634:18;1621:32;1696:5;1689:13;1682:21;1675:5;1672:32;1662:60;;1718:1;1715;1708:12;1662:60;1741:5;1731:15;;;1243:509;;;;;:::o;1757:348::-;1841:6;1894:2;1882:9;1873:7;1869:23;1865:32;1862:52;;;1910:1;1907;1900:12;1862:52;1950:9;1937:23;1983:18;1975:6;1972:30;1969:50;;;2015:1;2012;2005:12;1969:50;2038:61;2091:7;2082:6;2071:9;2067:22;2038:61;:::i;:::-;2028:71;1757:348;-1:-1:-1;;;;1757:348:542:o;2302:658::-;2473:2;2525:21;;;2595:13;;2498:18;;;2617:22;;;2444:4;;2473:2;2696:15;;;;2670:2;2655:18;;;2444:4;2739:195;2753:6;2750:1;2747:13;2739:195;;;2818:13;;-1:-1:-1;;;;;2814:39:542;2802:52;;2909:15;;;;2874:12;;;;2850:1;2768:9;2739:195;;;-1:-1:-1;2951:3:542;;2302:658;-1:-1:-1;;;;;;2302:658:542:o","linkReferences":{}},"methodIdentifiers":{"isEntitled(address[])":"ddc6e68e","setEntitled(address[],bool)":"3f4c4d83"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"user\",\"type\":\"address[]\"}],\"name\":\"isEntitled\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"user\",\"type\":\"address[]\"},{\"internalType\":\"bool\",\"name\":\"userIsEntitled\",\"type\":\"bool\"}],\"name\":\"setEntitled\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{\"isEntitled(address[])\":{\"params\":{\"user\":\"address of the user to check\"},\"returns\":{\"_0\":\"whether the user is entitled to the permission\"}}},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{\"isEntitled(address[])\":{\"notice\":\"checks whether a user is has a given permission for a channel or a space\"}},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/test/mocks/MockCustomEntitlement.sol\":\"MockCustomEntitlement\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/@openzeppelin/\",\":@prb/math/=lib/@prb/math/src/\",\":@prb/test/=lib/@prb/test/src/\",\":account-abstraction/=lib/account-abstraction/contracts/\",\":base64/=lib/base64/\",\":ds-test/=lib/ds-test/src/\",\":forge-std/=lib/forge-std/src/\",\":hardhat-deploy/=lib/hardhat-deploy/\",\":solady/=lib/solady/src/\"]},\"sources\":{\"contracts/src/spaces/entitlements/ICustomEntitlement.sol\":{\"keccak256\":\"0x228e49e82d252c14776d6230e33ca144f3356d691aa38f361b3e8c9581e1d746\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ad378879fbd8021da9861042be4249ad532646156352e444f177a3b941252e7a\",\"dweb:/ipfs/QmWcF7qipqDSwMDATBtwNDoyJtzA9zNsHzWwYwWf2rJqcc\"]},\"contracts/test/mocks/MockCustomEntitlement.sol\":{\"keccak256\":\"0x6afb663aa6c5f6403a9b9f833f79e5cc87f594e8bca8f91e5d69d835242655e2\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://0addd74396a3583bc3e6fd38977d84e06a98093567038ce067f505784cd7e38c\",\"dweb:/ipfs/Qma5CE3NqVK9cEczjwhtdGgtUr1P4qgGpa9DMh6druciyx\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address[]","name":"user","type":"address[]"}],"stateMutability":"view","type":"function","name":"isEntitled","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"address[]","name":"user","type":"address[]"},{"internalType":"bool","name":"userIsEntitled","type":"bool"}],"stateMutability":"nonpayable","type":"function","name":"setEntitled"}],"devdoc":{"kind":"dev","methods":{"isEntitled(address[])":{"params":{"user":"address of the user to check"},"returns":{"_0":"whether the user is entitled to the permission"}}},"version":1},"userdoc":{"kind":"user","methods":{"isEntitled(address[])":{"notice":"checks whether a user is has a given permission for a channel or a space"}},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/@openzeppelin/","@prb/math/=lib/@prb/math/src/","@prb/test/=lib/@prb/test/src/","account-abstraction/=lib/account-abstraction/contracts/","base64/=lib/base64/","ds-test/=lib/ds-test/src/","forge-std/=lib/forge-std/src/","hardhat-deploy/=lib/hardhat-deploy/","solady/=lib/solady/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"contracts/test/mocks/MockCustomEntitlement.sol":"MockCustomEntitlement"},"evmVersion":"paris","libraries":{}},"sources":{"contracts/src/spaces/entitlements/ICustomEntitlement.sol":{"keccak256":"0x228e49e82d252c14776d6230e33ca144f3356d691aa38f361b3e8c9581e1d746","urls":["bzz-raw://ad378879fbd8021da9861042be4249ad532646156352e444f177a3b941252e7a","dweb:/ipfs/QmWcF7qipqDSwMDATBtwNDoyJtzA9zNsHzWwYwWf2rJqcc"],"license":"MIT"},"contracts/test/mocks/MockCustomEntitlement.sol":{"keccak256":"0x6afb663aa6c5f6403a9b9f833f79e5cc87f594e8bca8f91e5d69d835242655e2","urls":["bzz-raw://0addd74396a3583bc3e6fd38977d84e06a98093567038ce067f505784cd7e38c","dweb:/ipfs/Qma5CE3NqVK9cEczjwhtdGgtUr1P4qgGpa9DMh6druciyx"],"license":"MIT"}},"version":1},"id":356} \ No newline at end of file +{"abi":[{"type":"constructor","inputs":[],"stateMutability":"nonpayable"},{"type":"function","name":"isEntitled","inputs":[{"name":"users","type":"address[]","internalType":"address[]"}],"outputs":[{"name":"","type":"bool","internalType":"bool"}],"stateMutability":"view"},{"type":"function","name":"setEntitled","inputs":[{"name":"users","type":"address[]","internalType":"address[]"},{"name":"userIsEntitled","type":"bool","internalType":"bool"}],"outputs":[],"stateMutability":"nonpayable"}],"bytecode":{"object":"0x608060405234801561001057600080fd5b50610317806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633f4c4d831461003b578063ddc6e68e14610050575b600080fd5b61004e61004936600461026d565b610077565b005b61006361005e3660046102c4565b610101565b604051901515815260200160405180910390f35b60005b82518110156100fc578160008085848151811061009957610099610301565b60200260200101516040516020016100c091906001600160a01b0391909116815260200190565b60408051808303601f19018152918152815160209283012083529082019290925201600020805460ff191691151591909117905560010161007a565b505050565b6000805b825181101561018d5760008084838151811061012357610123610301565b602002602001015160405160200161014a91906001600160a01b0391909116815260200190565b60408051601f198184030181529181528151602092830120835290820192909252016000205460ff1615156001036101855750600192915050565b600101610105565b50600092915050565b634e487b7160e01b600052604160045260246000fd5b80356001600160a01b03811681146101c357600080fd5b919050565b600082601f8301126101d957600080fd5b8135602067ffffffffffffffff808311156101f6576101f6610196565b8260051b604051601f19603f8301168101818110848211171561021b5761021b610196565b604052938452602081870181019490810192508785111561023b57600080fd5b6020870191505b8482101561026257610253826101ac565b83529183019190830190610242565b979650505050505050565b6000806040838503121561028057600080fd5b823567ffffffffffffffff81111561029757600080fd5b6102a3858286016101c8565b925050602083013580151581146102b957600080fd5b809150509250929050565b6000602082840312156102d657600080fd5b813567ffffffffffffffff8111156102ed57600080fd5b6102f9848285016101c8565b949350505050565b634e487b7160e01b600052603260045260246000fd","sourceMap":"152:582:356:-:0;;;247:16;;;;;;;;;;152:582;;;;;;","linkReferences":{}},"deployedBytecode":{"object":"0x608060405234801561001057600080fd5b50600436106100365760003560e01c80633f4c4d831461003b578063ddc6e68e14610050575b600080fd5b61004e61004936600461026d565b610077565b005b61006361005e3660046102c4565b610101565b604051901515815260200160405180910390f35b60005b82518110156100fc578160008085848151811061009957610099610301565b60200260200101516040516020016100c091906001600160a01b0391909116815260200190565b60408051808303601f19018152918152815160209283012083529082019290925201600020805460ff191691151591909117905560010161007a565b505050565b6000805b825181101561018d5760008084838151811061012357610123610301565b602002602001015160405160200161014a91906001600160a01b0391909116815260200190565b60408051601f198184030181529181528151602092830120835290820192909252016000205460ff1615156001036101855750600192915050565b600101610105565b50600092915050565b634e487b7160e01b600052604160045260246000fd5b80356001600160a01b03811681146101c357600080fd5b919050565b600082601f8301126101d957600080fd5b8135602067ffffffffffffffff808311156101f6576101f6610196565b8260051b604051601f19603f8301168101818110848211171561021b5761021b610196565b604052938452602081870181019490810192508785111561023b57600080fd5b6020870191505b8482101561026257610253826101ac565b83529183019190830190610242565b979650505050505050565b6000806040838503121561028057600080fd5b823567ffffffffffffffff81111561029757600080fd5b6102a3858286016101c8565b925050602083013580151581146102b957600080fd5b809150509250929050565b6000602082840312156102d657600080fd5b813567ffffffffffffffff8111156102ed57600080fd5b6102f9848285016101c8565b949350505050565b634e487b7160e01b600052603260045260246000fd","sourceMap":"152:582:356:-:0;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;267:201;;;;;;:::i;:::-;;:::i;:::-;;472:260;;;;;;:::i;:::-;;:::i;:::-;;;2275:14:542;;2268:22;2250:41;;2238:2;2223:18;472:260:356;;;;;;;267:201;353:9;348:116;372:5;:12;368:1;:16;348:116;;;443:14;399:8;:41;429:5;435:1;429:8;;;;;;;;:::i;:::-;;;;;;;418:20;;;;;;;-1:-1:-1;;;;;2598:32:542;;;;2580:51;;2568:2;2553:18;;2434:203;418:20:356;;;;;;;-1:-1:-1;;418:20:356;;;;;;408:31;;418:20;408:31;;;;399:41;;;;;;;;;;-1:-1:-1;399:41:356;:58;;-1:-1:-1;;399:58:356;;;;;;;;;;-1:-1:-1;386:3:356;348:116;;;;267:201;;:::o;472:260::-;556:4;;568:142;592:5;:12;588:1;:16;568:142;;;623:8;:41;653:5;659:1;653:8;;;;;;;;:::i;:::-;;;;;;;642:20;;;;;;;-1:-1:-1;;;;;2598:32:542;;;;2580:51;;2568:2;2553:18;;2434:203;642:20:356;;;;-1:-1:-1;;642:20:356;;;;;;;;;632:31;;642:20;632:31;;;;623:41;;;;;;;;;;-1:-1:-1;623:41:356;;;;:49;;:41;:49;619:85;;-1:-1:-1;691:4:356;;472:260;-1:-1:-1;;472:260:356:o;619:85::-;606:3;;568:142;;;-1:-1:-1;722:5:356;;472:260;-1:-1:-1;;472:260:356:o;14:127:542:-;75:10;70:3;66:20;63:1;56:31;106:4;103:1;96:15;130:4;127:1;120:15;146:173;214:20;;-1:-1:-1;;;;;263:31:542;;253:42;;243:70;;309:1;306;299:12;243:70;146:173;;;:::o;324:914::-;378:5;431:3;424:4;416:6;412:17;408:27;398:55;;449:1;446;439:12;398:55;485:6;472:20;511:4;534:18;571:2;567;564:10;561:36;;;577:18;;:::i;:::-;623:2;620:1;616:10;655:2;649:9;718:2;714:7;709:2;705;701:11;697:25;689:6;685:38;773:6;761:10;758:22;753:2;741:10;738:18;735:46;732:72;;;784:18;;:::i;:::-;820:2;813:22;870:18;;;916:4;948:15;;;944:26;;;904:17;;;;-1:-1:-1;982:15:542;;;979:35;;;1010:1;1007;1000:12;979:35;1046:4;1038:6;1034:17;1023:28;;1060:148;1076:6;1071:3;1068:15;1060:148;;;1142:23;1161:3;1142:23;:::i;:::-;1130:36;;1186:12;;;;1093;;;;1060:148;;;1226:6;324:914;-1:-1:-1;;;;;;;324:914:542:o;1243:509::-;1333:6;1341;1394:2;1382:9;1373:7;1369:23;1365:32;1362:52;;;1410:1;1407;1400:12;1362:52;1450:9;1437:23;1483:18;1475:6;1472:30;1469:50;;;1515:1;1512;1505:12;1469:50;1538:61;1591:7;1582:6;1571:9;1567:22;1538:61;:::i;:::-;1528:71;;;1649:2;1638:9;1634:18;1621:32;1696:5;1689:13;1682:21;1675:5;1672:32;1662:60;;1718:1;1715;1708:12;1662:60;1741:5;1731:15;;;1243:509;;;;;:::o;1757:348::-;1841:6;1894:2;1882:9;1873:7;1869:23;1865:32;1862:52;;;1910:1;1907;1900:12;1862:52;1950:9;1937:23;1983:18;1975:6;1972:30;1969:50;;;2015:1;2012;2005:12;1969:50;2038:61;2091:7;2082:6;2071:9;2067:22;2038:61;:::i;:::-;2028:71;1757:348;-1:-1:-1;;;;1757:348:542:o;2302:127::-;2363:10;2358:3;2354:20;2351:1;2344:31;2394:4;2391:1;2384:15;2418:4;2415:1;2408:15","linkReferences":{}},"methodIdentifiers":{"isEntitled(address[])":"ddc6e68e","setEntitled(address[],bool)":"3f4c4d83"},"rawMetadata":"{\"compiler\":{\"version\":\"0.8.24+commit.e11b9ed9\"},\"language\":\"Solidity\",\"output\":{\"abi\":[{\"inputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"constructor\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"users\",\"type\":\"address[]\"}],\"name\":\"isEntitled\",\"outputs\":[{\"internalType\":\"bool\",\"name\":\"\",\"type\":\"bool\"}],\"stateMutability\":\"view\",\"type\":\"function\"},{\"inputs\":[{\"internalType\":\"address[]\",\"name\":\"users\",\"type\":\"address[]\"},{\"internalType\":\"bool\",\"name\":\"userIsEntitled\",\"type\":\"bool\"}],\"name\":\"setEntitled\",\"outputs\":[],\"stateMutability\":\"nonpayable\",\"type\":\"function\"}],\"devdoc\":{\"kind\":\"dev\",\"methods\":{},\"version\":1},\"userdoc\":{\"kind\":\"user\",\"methods\":{},\"version\":1}},\"settings\":{\"compilationTarget\":{\"contracts/test/mocks/MockCustomEntitlement.sol\":\"MockCustomEntitlement\"},\"evmVersion\":\"paris\",\"libraries\":{},\"metadata\":{\"appendCBOR\":false,\"bytecodeHash\":\"none\"},\"optimizer\":{\"enabled\":true,\"runs\":200},\"remappings\":[\":@openzeppelin/=lib/@openzeppelin/\",\":@prb/math/=lib/@prb/math/src/\",\":@prb/test/=lib/@prb/test/src/\",\":account-abstraction/=lib/account-abstraction/contracts/\",\":ds-test/=lib/ds-test/src/\",\":forge-std/=lib/forge-std/src/\",\":hardhat-deploy/=lib/hardhat-deploy/\",\":solady/=lib/solady/src/\"]},\"sources\":{\"contracts/src/spaces/entitlements/ICustomEntitlement.sol\":{\"keccak256\":\"0x228e49e82d252c14776d6230e33ca144f3356d691aa38f361b3e8c9581e1d746\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://ad378879fbd8021da9861042be4249ad532646156352e444f177a3b941252e7a\",\"dweb:/ipfs/QmWcF7qipqDSwMDATBtwNDoyJtzA9zNsHzWwYwWf2rJqcc\"]},\"contracts/test/mocks/MockCustomEntitlement.sol\":{\"keccak256\":\"0x99a8c364152ef8f6a33bb93c8b2df6d29541a56cb2129a6d3260aecb1a0d0077\",\"license\":\"MIT\",\"urls\":[\"bzz-raw://b6a13501aadf349bb71309de5760dfe7971634e8dcff329e0ae8295b8830aace\",\"dweb:/ipfs/QmT8xRoZX8gCWaNo1BPZia9GrEkCtthuxpYFFJ1dwoy46Y\"]}},\"version\":1}","metadata":{"compiler":{"version":"0.8.24+commit.e11b9ed9"},"language":"Solidity","output":{"abi":[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address[]","name":"users","type":"address[]"}],"stateMutability":"view","type":"function","name":"isEntitled","outputs":[{"internalType":"bool","name":"","type":"bool"}]},{"inputs":[{"internalType":"address[]","name":"users","type":"address[]"},{"internalType":"bool","name":"userIsEntitled","type":"bool"}],"stateMutability":"nonpayable","type":"function","name":"setEntitled"}],"devdoc":{"kind":"dev","methods":{},"version":1},"userdoc":{"kind":"user","methods":{},"version":1}},"settings":{"remappings":["@openzeppelin/=lib/@openzeppelin/","@prb/math/=lib/@prb/math/src/","@prb/test/=lib/@prb/test/src/","account-abstraction/=lib/account-abstraction/contracts/","ds-test/=lib/ds-test/src/","forge-std/=lib/forge-std/src/","hardhat-deploy/=lib/hardhat-deploy/","solady/=lib/solady/src/"],"optimizer":{"enabled":true,"runs":200},"metadata":{"bytecodeHash":"none","appendCBOR":false},"compilationTarget":{"contracts/test/mocks/MockCustomEntitlement.sol":"MockCustomEntitlement"},"evmVersion":"paris","libraries":{}},"sources":{"contracts/src/spaces/entitlements/ICustomEntitlement.sol":{"keccak256":"0x228e49e82d252c14776d6230e33ca144f3356d691aa38f361b3e8c9581e1d746","urls":["bzz-raw://ad378879fbd8021da9861042be4249ad532646156352e444f177a3b941252e7a","dweb:/ipfs/QmWcF7qipqDSwMDATBtwNDoyJtzA9zNsHzWwYwWf2rJqcc"],"license":"MIT"},"contracts/test/mocks/MockCustomEntitlement.sol":{"keccak256":"0x99a8c364152ef8f6a33bb93c8b2df6d29541a56cb2129a6d3260aecb1a0d0077","urls":["bzz-raw://b6a13501aadf349bb71309de5760dfe7971634e8dcff329e0ae8295b8830aace","dweb:/ipfs/QmT8xRoZX8gCWaNo1BPZia9GrEkCtthuxpYFFJ1dwoy46Y"],"license":"MIT"}},"version":1},"id":356} diff --git a/packages/generated/dev/abis/MockCustomEntitlement.metadata.json b/packages/generated/dev/abis/MockCustomEntitlement.metadata.json index b417cfebe..0eaf195b6 100644 --- a/packages/generated/dev/abis/MockCustomEntitlement.metadata.json +++ b/packages/generated/dev/abis/MockCustomEntitlement.metadata.json @@ -14,7 +14,7 @@ "inputs": [ { "internalType": "address[]", - "name": "user", + "name": "users", "type": "address[]" } ], @@ -33,7 +33,7 @@ "inputs": [ { "internalType": "address[]", - "name": "user", + "name": "users", "type": "address[]" }, { @@ -49,25 +49,12 @@ ], "devdoc": { "kind": "dev", - "methods": { - "isEntitled(address[])": { - "params": { - "user": "address of the user to check" - }, - "returns": { - "_0": "whether the user is entitled to the permission" - } - } - }, + "methods": {}, "version": 1 }, "userdoc": { "kind": "user", - "methods": { - "isEntitled(address[])": { - "notice": "checks whether a user is has a given permission for a channel or a space" - } - }, + "methods": {}, "version": 1 } }, @@ -77,7 +64,6 @@ "@prb/math/=lib/@prb/math/src/", "@prb/test/=lib/@prb/test/src/", "account-abstraction/=lib/account-abstraction/contracts/", - "base64/=lib/base64/", "ds-test/=lib/ds-test/src/", "forge-std/=lib/forge-std/src/", "hardhat-deploy/=lib/hardhat-deploy/", @@ -107,10 +93,10 @@ "license": "MIT" }, "contracts/test/mocks/MockCustomEntitlement.sol": { - "keccak256": "0x6afb663aa6c5f6403a9b9f833f79e5cc87f594e8bca8f91e5d69d835242655e2", + "keccak256": "0x99a8c364152ef8f6a33bb93c8b2df6d29541a56cb2129a6d3260aecb1a0d0077", "urls": [ - "bzz-raw://0addd74396a3583bc3e6fd38977d84e06a98093567038ce067f505784cd7e38c", - "dweb:/ipfs/Qma5CE3NqVK9cEczjwhtdGgtUr1P4qgGpa9DMh6druciyx" + "bzz-raw://b6a13501aadf349bb71309de5760dfe7971634e8dcff329e0ae8295b8830aace", + "dweb:/ipfs/QmT8xRoZX8gCWaNo1BPZia9GrEkCtthuxpYFFJ1dwoy46Y" ], "license": "MIT" } diff --git a/packages/sdk/src/channelsWithEntitlements.test.ts b/packages/sdk/src/channelsWithEntitlements.test.ts index 9a74acc4d..0ef4572dd 100644 --- a/packages/sdk/src/channelsWithEntitlements.test.ts +++ b/packages/sdk/src/channelsWithEntitlements.test.ts @@ -15,6 +15,8 @@ import { everyoneMembershipStruct, linkWallets, getXchainSupportedRpcUrlsForTesting, + erc20CheckOp, + customCheckOp, } from './util.test' import { MembershipOp } from '@river-build/proto' import { makeUserStreamId } from './id' @@ -24,10 +26,9 @@ import { NoopRuleData, IRuleEntitlementBase, Permission, - getContractAddress, - publicMint, - burn, - balanceOf, + TestERC721, + TestERC20, + TestCustomEntitlement, LogicalOperationType, OperationType, Operation, @@ -487,7 +488,7 @@ describe('channelsWithEntitlements', () => { }) test('oneNftGateJoinPass - join as root, asset in linked wallet', async () => { - const testNft1Address = await getContractAddress('TestNFT1') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') const { alice, bob, @@ -507,7 +508,7 @@ describe('channelsWithEntitlements', () => { // Mint the needed asset to Alice's linked wallet log('Minting an NFT for carols wallet, which is linked to alices wallet') - await publicMint('TestNFT1', carolsWallet.address as Address) + await TestERC721.publicMint('TestNFT1', carolsWallet.address as Address) // Wait 2 seconds for the negative auth cache to expire await new Promise((f) => setTimeout(f, 2000)) @@ -523,7 +524,7 @@ describe('channelsWithEntitlements', () => { }) test('oneNftGateJoinPass - join as linked wallet, asset in root wallet', async () => { - const testNft1Address = await getContractAddress('TestNFT1') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') const { alice, bob, @@ -540,7 +541,7 @@ describe('channelsWithEntitlements', () => { await linkWallets(carolSpaceDapp, carolProvider.wallet, aliceProvider.wallet) log('Minting an NFT for carols wallet, which is the root to alices wallet') - await publicMint('TestNFT1', carolsWallet.address as Address) + await TestERC721.publicMint('TestNFT1', carolsWallet.address as Address) log('expect that alice can join the space') // Validate alice can join the channel @@ -554,7 +555,7 @@ describe('channelsWithEntitlements', () => { }) test('oneNftGateJoinPass', async () => { - const testNftAddress = await getContractAddress('TestNFT') + const testNftAddress = await TestERC721.getContractAddress('TestNFT') const { alice, alicesWallet, aliceSpaceDapp, bob, spaceId, channelId } = await setupChannelWithCustomRole([], getNftRuleData(testNftAddress)) @@ -562,7 +563,7 @@ describe('channelsWithEntitlements', () => { await expectUserCannotJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) // Mint an nft for alice - she should be able to join now - await publicMint('TestNFT', alicesWallet.address as Address) + await TestERC721.publicMint('TestNFT', alicesWallet.address as Address) // Wait 2 seconds for the negative auth cache to expire await new Promise((f) => setTimeout(f, 2000)) @@ -575,12 +576,12 @@ describe('channelsWithEntitlements', () => { }) test('user booted on key request after entitlement loss', async () => { - const testNftAddress = await getContractAddress('TestNFT') + const testNftAddress = await TestERC721.getContractAddress('TestNFT') const { alice, alicesWallet, aliceSpaceDapp, bob, spaceId, channelId } = await setupChannelWithCustomRole([], getNftRuleData(testNftAddress)) // Mint an nft for alice - she should be able to join now - const tokenId = await publicMint('TestNFT', alicesWallet.address as Address) + const tokenId = await TestERC721.publicMint('TestNFT', alicesWallet.address as Address) // Validate alice can join the channel await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) @@ -593,8 +594,10 @@ describe('channelsWithEntitlements', () => { // Burn Alice's NFT and validate her zero balance. She should now fail an entitlement check for the // channel. - await burn('TestNFT', tokenId) - await expect(balanceOf('TestNFT', alicesWallet.address as Address)).resolves.toBe(0) + await TestERC721.burn('TestNFT', tokenId) + await expect( + TestERC721.balanceOf('TestNFT', alicesWallet.address as Address), + ).resolves.toBe(0) // Wait 5 seconds for the positive auth cache to expire await new Promise((f) => setTimeout(f, 5000)) @@ -634,7 +637,7 @@ describe('channelsWithEntitlements', () => { }) test('user cannot post after entitlement loss', async () => { - const testNftAddress = await getContractAddress('TestNFT') + const testNftAddress = await TestERC721.getContractAddress('TestNFT') const { alice, alicesWallet, aliceSpaceDapp, bob, spaceId, channelId } = await setupChannelWithCustomRole([], getNftRuleData(testNftAddress), [ Permission.Read, @@ -642,7 +645,7 @@ describe('channelsWithEntitlements', () => { ]) // Mint an nft for alice - she should be able to join now - const tokenId = await publicMint('TestNFT', alicesWallet.address as Address) + const tokenId = await TestERC721.publicMint('TestNFT', alicesWallet.address as Address) // Validate alice can join the channel await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) @@ -655,8 +658,10 @@ describe('channelsWithEntitlements', () => { // Burn Alice's NFT and validate her zero balance. She should now fail an entitlement check for the // channel. - await burn('TestNFT', tokenId) - await expect(balanceOf('TestNFT', alicesWallet.address as Address)).resolves.toBe(0) + await TestERC721.burn('TestNFT', tokenId) + await expect( + TestERC721.balanceOf('TestNFT', alicesWallet.address as Address), + ).resolves.toBe(0) // Wait 5 seconds for the positive auth cache to expire await new Promise((f) => setTimeout(f, 5000)) @@ -673,7 +678,7 @@ describe('channelsWithEntitlements', () => { }) test('oneNftGateJoinFail', async () => { - const testNft1Address = await getContractAddress('TestNFT1') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') const { alice, aliceSpaceDapp, bob, spaceId, channelId } = await setupChannelWithCustomRole( [], getNftRuleData(testNft1Address), @@ -688,13 +693,13 @@ describe('channelsWithEntitlements', () => { }) test('twoNftGateJoinPass', async () => { - const testNft1Address = await getContractAddress('TestNFT1') - const testNft2Address = await getContractAddress('TestNFT2') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') + const testNft2Address = await TestERC721.getContractAddress('TestNFT2') const { alice, bob, alicesWallet, aliceSpaceDapp, spaceId, channelId } = await setupChannelWithCustomRole([], twoNftRuleData(testNft1Address, testNft2Address)) - const aliceMintTx1 = publicMint('TestNFT1', alicesWallet.address as Address) - const aliceMintTx2 = publicMint('TestNFT2', alicesWallet.address as Address) + const aliceMintTx1 = TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) + const aliceMintTx2 = TestERC721.publicMint('TestNFT2', alicesWallet.address as Address) log('Minting nfts for alice') await Promise.all([aliceMintTx1, aliceMintTx2]) @@ -711,8 +716,8 @@ describe('channelsWithEntitlements', () => { }) test('twoNftGateJoinPass - acrossLinkedWallets', async () => { - const testNft1Address = await getContractAddress('TestNFT1') - const testNft2Address = await getContractAddress('TestNFT2') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') + const testNft2Address = await TestERC721.getContractAddress('TestNFT2') const { alice, bob, @@ -725,8 +730,8 @@ describe('channelsWithEntitlements', () => { channelId, } = await setupChannelWithCustomRole([], twoNftRuleData(testNft1Address, testNft2Address)) - const aliceMintTx1 = publicMint('TestNFT1', alicesWallet.address as Address) - const carolMintTx2 = publicMint('TestNFT2', carolsWallet.address as Address) + const aliceMintTx1 = TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) + const carolMintTx2 = TestERC721.publicMint('TestNFT2', carolsWallet.address as Address) log('Minting nfts for alice and carol') await Promise.all([aliceMintTx1, carolMintTx2]) @@ -745,14 +750,14 @@ describe('channelsWithEntitlements', () => { }) test('twoNftGateJoinFail', async () => { - const testNft1Address = await getContractAddress('TestNFT1') - const testNft2Address = await getContractAddress('TestNFT2') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') + const testNft2Address = await TestERC721.getContractAddress('TestNFT2') const { alice, aliceSpaceDapp, bob, alicesWallet, spaceId, channelId } = await setupChannelWithCustomRole([], twoNftRuleData(testNft1Address, testNft2Address)) // Mint only one of the required NFTs for alice log('Minting only one of two required NFTs for alice') - await publicMint('TestNFT1', alicesWallet.address as Address) + await TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) log('expect that alice cannot join the channel') await expectUserCannotJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) @@ -763,8 +768,8 @@ describe('channelsWithEntitlements', () => { }) test('OrOfTwoNftGateJoinPass', async () => { - const testNft1Address = await getContractAddress('TestNFT1') - const testNft2Address = await getContractAddress('TestNFT2') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') + const testNft2Address = await TestERC721.getContractAddress('TestNFT2') const { alice, bob, alicesWallet, aliceSpaceDapp, spaceId, channelId } = await setupChannelWithCustomRole( [], @@ -772,7 +777,7 @@ describe('channelsWithEntitlements', () => { ) // join alice log('Minting an NFT for alice') - await publicMint('TestNFT1', alicesWallet.address as Address) + await TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) log('expect that alice can join the channel') await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) @@ -785,9 +790,9 @@ describe('channelsWithEntitlements', () => { }) test('orOfTwoNftOrOneNftGateJoinPass', async () => { - const testNft1Address = await getContractAddress('TestNFT1') - const testNft2Address = await getContractAddress('TestNFT2') - const testNft3Address = await getContractAddress('TestNFT3') + const testNft1Address = await TestERC721.getContractAddress('TestNFT1') + const testNft2Address = await TestERC721.getContractAddress('TestNFT2') + const testNft3Address = await TestERC721.getContractAddress('TestNFT3') const leftOperation: Operation = { opType: OperationType.CHECK, checkType: CheckOperationType.ERC721, @@ -828,8 +833,8 @@ describe('channelsWithEntitlements', () => { await setupChannelWithCustomRole([], ruleData) log("Mint Alice's NFTs") - const aliceMintTx1 = publicMint('TestNFT1', alicesWallet.address as Address) - const aliceMintTx2 = publicMint('TestNFT2', alicesWallet.address as Address) + const aliceMintTx1 = TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) + const aliceMintTx2 = TestERC721.publicMint('TestNFT2', alicesWallet.address as Address) await Promise.all([aliceMintTx1, aliceMintTx2]) log('expect that alice can join the channel') @@ -842,6 +847,260 @@ describe('channelsWithEntitlements', () => { log('Done', Date.now() - doneStart) }) + test('erc20 gate join pass', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + + const { alice, bob, alicesWallet, aliceSpaceDapp, spaceId, channelId } = + await setupChannelWithCustomRole([], ruleData) + + await TestERC20.publicMint('TestERC20', alicesWallet.address as Address, 100) + + log('expect that alice can join the channel') + await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + // kill the clients + const doneStart = Date.now() + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('erc20 gate join fail', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + + const { alice, bob, aliceSpaceDapp, spaceId, channelId } = await setupChannelWithCustomRole( + [], + ruleData, + ) + + log('expect that alice cannot join the channel') + await expectUserCannotJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + // kill the clients + const doneStart = Date.now() + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('ERC20 gate join pass - join as root, asset in linked wallet', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + const { + alice, + bob, + aliceSpaceDapp, + aliceProvider, + carolsWallet, + carolProvider, + spaceId, + channelId, + } = await setupChannelWithCustomRole([], ruleData) + + // Link carol's wallet to alice's as root + await linkWallets(aliceSpaceDapp, aliceProvider.wallet, carolProvider.wallet) + + // Validate alice cannot join the channel + await expectUserCannotJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + // Mint the needed asset to Alice's linked wallet + log('Minting 50 ERC20 tokens for carols wallet, which is linked to alices wallet') + await TestERC20.publicMint('TestERC20', carolsWallet.address as Address, 50) + + // Wait 2 seconds for the negative auth cache to expire + await new Promise((f) => setTimeout(f, 2000)) + + // Validate alice can join the channel + await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('ERC20 Gate Join Pass - join as linked wallet, assets in root wallet', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + const { + alice, + bob, + aliceSpaceDapp, + carolSpaceDapp, + aliceProvider, + carolsWallet, + carolProvider, + spaceId, + channelId, + } = await setupChannelWithCustomRole([], ruleData) + + log("Joining alice's wallet as a linked wallet to carols root wallet") + await linkWallets(carolSpaceDapp, carolProvider.wallet, aliceProvider.wallet) + + log('Minting an NFT for carols wallet, which is the root to alices wallet') + await TestERC20.publicMint('TestERC20', carolsWallet.address as Address, 50) + + log('expect that alice can join the space') + // Validate alice can join the channel + await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('ERC20 Gate Join Pass - assets split across wallets', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + const { + alice, + bob, + aliceSpaceDapp, + carolSpaceDapp, + aliceProvider, + carolsWallet, + carolProvider, + spaceId, + channelId, + } = await setupChannelWithCustomRole([], ruleData) + + log("Joining alice's wallet as a linked wallet to carols root wallet") + await linkWallets(carolSpaceDapp, carolProvider.wallet, aliceProvider.wallet) + + log('Minting an NFT for carols wallet, which is the root to alices wallet') + await TestERC20.publicMint('TestERC20', carolsWallet.address as Address, 25) + await TestERC20.publicMint('TestERC20', aliceProvider.wallet.address as Address, 25) + + log('expect that alice can join the space') + // Validate alice can join the channel + await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('custom entitlement gate pass', async () => { + const ruleData = treeToRuleData(await customCheckOp('TestCustom')) + + const { alice, bob, alicesWallet, aliceSpaceDapp, spaceId, channelId } = + await setupChannelWithCustomRole([], ruleData) + + await TestCustomEntitlement.setEntitled( + 'TestCustom', + [alicesWallet.address as Address], + true, + ) + + log('expect that alice can join the channel') + await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + // kill the clients + const doneStart = Date.now() + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('custom entitlement gate fail', async () => { + const ruleData = treeToRuleData(await customCheckOp('TestCustom')) + + const { alice, bob, alicesWallet, aliceSpaceDapp, spaceId, channelId } = + await setupChannelWithCustomRole([], ruleData) + + await TestCustomEntitlement.setEntitled( + 'TestCustom', + [alicesWallet.address as Address], + false, + ) + + log('expect that alice can join the channel') + await expectUserCannotJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + // kill the clients + const doneStart = Date.now() + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('custom entitlement gate join pass - join as root, linked wallet entitled', async () => { + const ruleData = treeToRuleData(await customCheckOp('TestCustom')) + + const { + alice, + bob, + aliceSpaceDapp, + aliceProvider, + carolsWallet, + carolProvider, + spaceId, + channelId, + } = await setupChannelWithCustomRole([], ruleData) + + // Link carol's wallet to alice's as root + await linkWallets(aliceSpaceDapp, aliceProvider.wallet, carolProvider.wallet) + + // Validate alice cannot join the channel + await expectUserCannotJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + // Set entitlement for carol's wallet + await TestCustomEntitlement.setEntitled( + 'TestCustom', + [carolsWallet.address as Address], + true, + ) + + // Wait 2 seconds for the negative auth cache to expire + await new Promise((f) => setTimeout(f, 2000)) + + // Validate alice can join the channel + await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('custom entitlement gated join - join as linked wallet, assets in root wallet', async () => { + const ruleData = treeToRuleData(await customCheckOp('TestCustom')) + const { + alice, + bob, + aliceSpaceDapp, + carolSpaceDapp, + aliceProvider, + carolsWallet, + carolProvider, + spaceId, + channelId, + } = await setupChannelWithCustomRole([], ruleData) + + log("Joining alice's wallet as a linked wallet to carols root wallet") + await linkWallets(carolSpaceDapp, carolProvider.wallet, aliceProvider.wallet) + + // Set carol's wallet as entitled + await TestCustomEntitlement.setEntitled( + 'TestCustom', + [carolsWallet.address as Address], + true, + ) + + log('expect that alice can join the space') + // Validate alice can join the channel + await expectUserCanJoinChannel(alice, aliceSpaceDapp, spaceId, channelId!) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + // Banning with entitlements — users need permission to ban other users. test('adminsCanRedactChannelMessages', async () => { // log('start adminsCanRedactChannelMessages') diff --git a/packages/sdk/src/spaceWithEntitlements.test.ts b/packages/sdk/src/spaceWithEntitlements.test.ts index 6bc66678e..749be6888 100644 --- a/packages/sdk/src/spaceWithEntitlements.test.ts +++ b/packages/sdk/src/spaceWithEntitlements.test.ts @@ -14,6 +14,8 @@ import { getNftRuleData, twoNftRuleData, getXchainSupportedRpcUrlsForTesting, + erc20CheckOp, + customCheckOp, } from './util.test' import { dlog } from '@river-build/dlog' import { MembershipOp } from '@river-build/proto' @@ -28,8 +30,9 @@ import { Operation, OperationType, Permission, - getContractAddress, - publicMint, + TestCustomEntitlement, + TestERC20, + TestERC721, treeToRuleData, IRuleEntitlementBase, ISpaceDapp, @@ -145,9 +148,9 @@ describe('spaceWithEntitlements', () => { let testNft1Address: string, testNft2Address: string, testNft3Address: string beforeAll(async () => { ;[testNft1Address, testNft2Address, testNft3Address] = await Promise.all([ - getContractAddress('TestNFT1'), - getContractAddress('TestNFT2'), - getContractAddress('TestNFT3'), + TestERC721.getContractAddress('TestNFT1'), + TestERC721.getContractAddress('TestNFT2'), + TestERC721.getContractAddress('TestNFT3'), ]) }) @@ -426,7 +429,7 @@ describe('spaceWithEntitlements', () => { // join alice log('Minting an NFT for carols wallet, which is linked to alices wallet') - await publicMint('TestNFT1', carolsWallet.address as Address) + await TestERC721.publicMint('TestNFT1', carolsWallet.address as Address) await expectUserCanJoin( spaceId, @@ -468,7 +471,7 @@ describe('spaceWithEntitlements', () => { // join alice log('Minting an NFT for carols wallet, which is the root to alices wallet') - await publicMint('TestNFT1', carolsWallet.address as Address) + await TestERC721.publicMint('TestNFT1', carolsWallet.address as Address) log('expect that alice can join the space') await expectUserCanJoin( @@ -498,7 +501,7 @@ describe('spaceWithEntitlements', () => { // join alice log('Minting an NFT for alice') - await publicMint('TestNFT1', alicesWallet.address as Address) + await TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) await expectUserCanJoin( spaceId, @@ -559,8 +562,8 @@ describe('spaceWithEntitlements', () => { ruleData: twoNftRuleData(testNft1Address, testNft2Address), }) - const aliceMintTx1 = publicMint('TestNFT1', alicesWallet.address as Address) - const aliceMintTx2 = publicMint('TestNFT2', alicesWallet.address as Address) + const aliceMintTx1 = TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) + const aliceMintTx2 = TestERC721.publicMint('TestNFT2', alicesWallet.address as Address) log('Minting nfts for alice') await Promise.all([aliceMintTx1, aliceMintTx2]) @@ -600,8 +603,8 @@ describe('spaceWithEntitlements', () => { ruleData: twoNftRuleData(testNft1Address, testNft2Address), }) - const aliceMintTx1 = publicMint('TestNFT1', alicesWallet.address as Address) - const carolMintTx2 = publicMint('TestNFT2', carolsWallet.address as Address) + const aliceMintTx1 = TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) + const carolMintTx2 = TestERC721.publicMint('TestNFT2', carolsWallet.address as Address) log('Minting nfts for alice and carol') await Promise.all([aliceMintTx1, carolMintTx2]) @@ -637,7 +640,7 @@ describe('spaceWithEntitlements', () => { // join alice log('Minting an NFT for alice') - await publicMint('TestNFT1', alicesWallet.address as Address) + await TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) // first join the space on chain const aliceJoinStart = Date.now() @@ -677,7 +680,7 @@ describe('spaceWithEntitlements', () => { // join alice log('Minting an NFT for alice') - await publicMint('TestNFT1', alicesWallet.address as Address) + await TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) // first join the space on chain log('Expect alice can join space') @@ -743,8 +746,8 @@ describe('spaceWithEntitlements', () => { }) log("Mint Alice's NFTs") - const aliceMintTx1 = publicMint('TestNFT1', alicesWallet.address as Address) - const aliceMintTx2 = publicMint('TestNFT2', alicesWallet.address as Address) + const aliceMintTx1 = TestERC721.publicMint('TestNFT1', alicesWallet.address as Address) + const aliceMintTx2 = TestERC721.publicMint('TestNFT2', alicesWallet.address as Address) await Promise.all([aliceMintTx1, aliceMintTx2]) log('expect alice can join space') @@ -764,4 +767,359 @@ describe('spaceWithEntitlements', () => { await alice.stopSync() log('Done', Date.now() - doneStart) }) + + test('erc20GateJoinPass', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + + const { alice, bob, aliceSpaceDapp, aliceProvider, alicesWallet, spaceId, channelId } = + await createTownWithRequirements({ + everyone: false, + users: [], + ruleData, + }) + + // join alice + log('Minting 100 ERC20 tokens for alice') + await TestERC20.publicMint('TestERC20', alicesWallet.address as Address, 100) + + await expectUserCanJoin( + spaceId, + channelId, + 'alice', + alice, + aliceSpaceDapp, + alicesWallet.address, + aliceProvider.wallet, + ) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('erc20GateJoinFail', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + + const { alice, bob, aliceSpaceDapp, aliceProvider, alicesWallet, spaceId } = + await createTownWithRequirements({ + everyone: false, + users: [], + ruleData, + }) + + // Have alice create her own space so she can initialize her user stream. + // Then she will attempt to join the space from the client, which should fail. + await createUserStreamAndSyncClient( + alice, + aliceSpaceDapp, + 'alice', + await everyoneMembershipStruct(aliceSpaceDapp, alice), + aliceProvider.wallet, + ) + + await expectUserCannotJoinSpace(spaceId, alice, aliceSpaceDapp, alicesWallet.address) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('erc20GateJoinPass - join as root, asset in linked wallet', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + const { + alice, + bob, + aliceSpaceDapp, + aliceProvider, + alicesWallet, + carolsWallet, + carolProvider, + spaceId, + channelId, + } = await createTownWithRequirements({ + everyone: false, + users: [], + ruleData: ruleData, + }) + + await linkWallets(aliceSpaceDapp, aliceProvider.wallet, carolProvider.wallet) + + // join alice + log('Minting 50 ERC20 tokens for carols wallet, which is linked to alices wallet') + await TestERC20.publicMint('TestERC20', carolsWallet.address as Address, 50) + + await expectUserCanJoin( + spaceId, + channelId, + 'alice', + alice, + aliceSpaceDapp, + alicesWallet.address, + aliceProvider.wallet, + ) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('erc20GateJoinPass - join as linked wallet, asset in root wallet', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + const { + alice, + bob, + aliceSpaceDapp, + aliceProvider, + alicesWallet, + carolsWallet, + carolProvider, + carolSpaceDapp, + spaceId, + channelId, + } = await createTownWithRequirements({ + everyone: false, + users: [], + ruleData: ruleData, + }) + + log("Joining alice's wallet as a linked wallet to carols root wallet") + await linkWallets(carolSpaceDapp, carolProvider.wallet, aliceProvider.wallet) + + // join alice + log('Minting an NFT for carols wallet, which is the root to alices wallet') + await TestERC20.publicMint('TestERC20', carolsWallet.address as Address, 50) + + log('expect that alice can join the space') + await expectUserCanJoin( + spaceId, + channelId, + 'alice', + alice, + aliceSpaceDapp, + alicesWallet.address, + aliceProvider.wallet, + ) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('erc20GateJoinPass - assets across wallets', async () => { + const ruleData = treeToRuleData(await erc20CheckOp('TestERC20', 50n)) + const { + alice, + bob, + aliceSpaceDapp, + aliceProvider, + alicesWallet, + carolsWallet, + carolProvider, + carolSpaceDapp, + spaceId, + channelId, + } = await createTownWithRequirements({ + everyone: false, + users: [], + ruleData: ruleData, + }) + + log("Joining alice's wallet as a linked wallet to carols root wallet") + await linkWallets(carolSpaceDapp, carolProvider.wallet, aliceProvider.wallet) + + // join alice + log('Minting an NFT for carols wallet, which is the root to alices wallet') + await TestERC20.publicMint('TestERC20', carolsWallet.address as Address, 25) + await TestERC20.publicMint('TestERC20', alicesWallet.address as Address, 25) + + log('expect that alice can join the space') + await expectUserCanJoin( + spaceId, + channelId, + 'alice', + alice, + aliceSpaceDapp, + alicesWallet.address, + aliceProvider.wallet, + ) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('customEntitlementGateJoinPass', async () => { + const ruleData = treeToRuleData(await customCheckOp('TestCustom')) + + const { alice, bob, aliceSpaceDapp, aliceProvider, alicesWallet, spaceId, channelId } = + await createTownWithRequirements({ + everyone: false, + users: [], + ruleData, + }) + + // set alice as entitled; she should be able to join. + await TestCustomEntitlement.setEntitled( + 'TestCustom', + [alicesWallet.address as Address], + true, + ) + + await expectUserCanJoin( + spaceId, + channelId, + 'alice', + alice, + aliceSpaceDapp, + alicesWallet.address, + aliceProvider.wallet, + ) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('customEntitlementGateJoinFail', async () => { + const ruleData = treeToRuleData(await customCheckOp('TestCustom')) + + const { alice, bob, aliceSpaceDapp, aliceProvider, alicesWallet, spaceId } = + await createTownWithRequirements({ + everyone: false, + users: [], + ruleData, + }) + + // Have alice create her own space so she can initialize her user stream. + // Then she will attempt to join the space from the client, which should fail. + await createUserStreamAndSyncClient( + alice, + aliceSpaceDapp, + 'alice', + await everyoneMembershipStruct(aliceSpaceDapp, alice), + aliceProvider.wallet, + ) + + await expectUserCannotJoinSpace(spaceId, alice, aliceSpaceDapp, alicesWallet.address) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('customEntitlementGateJoinPass - join as root, asset in linked wallet', async () => { + const ruleData = treeToRuleData(await customCheckOp('TestCustom')) + const { + alice, + bob, + aliceSpaceDapp, + aliceProvider, + alicesWallet, + carolsWallet, + carolProvider, + spaceId, + channelId, + } = await createTownWithRequirements({ + everyone: false, + users: [], + ruleData: ruleData, + }) + + await linkWallets(aliceSpaceDapp, aliceProvider.wallet, carolProvider.wallet) + + // join alice + log("Setting carol's wallet as entitled") + await TestCustomEntitlement.setEntitled( + 'TestCustom', + [carolsWallet.address as Address], + true, + ) + + await expectUserCanJoin( + spaceId, + channelId, + 'alice', + alice, + aliceSpaceDapp, + alicesWallet.address, + aliceProvider.wallet, + ) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) + + test('customEntitlementGateJoinPass - join as linked wallet, asset in root wallet', async () => { + const contractName = 'TestCustom' + const customAddress = await TestCustomEntitlement.getContractAddress(contractName) + const op: Operation = { + opType: OperationType.CHECK, + checkType: CheckOperationType.ISENTITLED, + chainId: 31337n, + contractAddress: customAddress, + threshold: 0n, + } + const ruleData = treeToRuleData(op) + const { + alice, + bob, + aliceSpaceDapp, + aliceProvider, + alicesWallet, + carolsWallet, + carolProvider, + carolSpaceDapp, + spaceId, + channelId, + } = await createTownWithRequirements({ + everyone: false, + users: [], + ruleData: ruleData, + }) + + log("Joining alice's wallet as a linked wallet to carols root wallet") + await linkWallets(carolSpaceDapp, carolProvider.wallet, aliceProvider.wallet) + + // join alice + log("Setting carol's linked wallet as entitled") + await TestCustomEntitlement.setEntitled( + contractName, + [carolsWallet.address as Address], + true, + ) + + log('expect that alice can join the space') + await expectUserCanJoin( + spaceId, + channelId, + 'alice', + alice, + aliceSpaceDapp, + alicesWallet.address, + aliceProvider.wallet, + ) + + const doneStart = Date.now() + // kill the clients + await bob.stopSync() + await alice.stopSync() + log('Done', Date.now() - doneStart) + }) }) diff --git a/packages/sdk/src/util.test.ts b/packages/sdk/src/util.test.ts index 86b1ce628..e3dda59c9 100644 --- a/packages/sdk/src/util.test.ts +++ b/packages/sdk/src/util.test.ts @@ -54,6 +54,8 @@ import { OperationType, treeToRuleData, SpaceDapp, + TestERC20, + TestCustomEntitlement, } from '@river-build/web3' const log = dlog('csb:test:util') @@ -129,6 +131,28 @@ export const getXchainSupportedRpcUrlsForTesting = (): string[] => { return ['http://127.0.0.1:8545', 'http://127.0.0.1:8546'] } +export async function erc20CheckOp(contractName: string, threshold: bigint): Promise { + const contractAddress = await TestERC20.getContractAddress(contractName) + return { + opType: OperationType.CHECK, + checkType: CheckOperationType.ERC20, + chainId: 31337n, + contractAddress, + threshold, + } +} + +export async function customCheckOp(contractName: string): Promise { + const contractAddress = await TestCustomEntitlement.getContractAddress(contractName) + return { + opType: OperationType.CHECK, + checkType: CheckOperationType.ISENTITLED, + chainId: 31337n, + contractAddress, + threshold: 0n, + } +} + /** * makeUniqueSpaceStreamId - space stream ids are derived from the contract * in tests without entitlements there are no contracts, so we use a random id diff --git a/packages/web3/src/MockCustomEntitlement.ts b/packages/web3/src/MockCustomEntitlement.ts new file mode 100644 index 000000000..ce41b9e21 --- /dev/null +++ b/packages/web3/src/MockCustomEntitlement.ts @@ -0,0 +1,55 @@ +import type { Abi } from 'abitype' +import { Address } from './ContractTypes' + +const _abi: Abi = [ + { + type: 'constructor', + inputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'isEntitled', + inputs: [ + { + name: 'users', + type: 'address[]', + internalType: 'address[]', + }, + ], + outputs: [ + { + name: '', + type: 'bool', + internalType: 'bool', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'setEntitled', + inputs: [ + { + name: 'users', + type: 'address[]', + internalType: 'address[]', + }, + { + name: 'userIsEntitled', + type: 'bool', + internalType: 'bool', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, +] as const + +const _bytecode: Address = + '0x608060405234801561001057600080fd5b50610317806100206000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80633f4c4d831461003b578063ddc6e68e14610050575b600080fd5b61004e61004936600461026d565b610077565b005b61006361005e3660046102c4565b610101565b604051901515815260200160405180910390f35b60005b82518110156100fc578160008085848151811061009957610099610301565b60200260200101516040516020016100c091906001600160a01b0391909116815260200190565b60408051808303601f19018152918152815160209283012083529082019290925201600020805460ff191691151591909117905560010161007a565b505050565b6000805b825181101561018d5760008084838151811061012357610123610301565b602002602001015160405160200161014a91906001600160a01b0391909116815260200190565b60408051601f198184030181529181528151602092830120835290820192909252016000205460ff1615156001036101855750600192915050565b600101610105565b50600092915050565b634e487b7160e01b600052604160045260246000fd5b80356001600160a01b03811681146101c357600080fd5b919050565b600082601f8301126101d957600080fd5b8135602067ffffffffffffffff808311156101f6576101f6610196565b8260051b604051601f19603f8301168101818110848211171561021b5761021b610196565b604052938452602081870181019490810192508785111561023b57600080fd5b6020870191505b8482101561026257610253826101ac565b83529183019190830190610242565b979650505050505050565b6000806040838503121561028057600080fd5b823567ffffffffffffffff81111561029757600080fd5b6102a3858286016101c8565b925050602083013580151581146102b957600080fd5b809150509250929050565b6000602082840312156102d657600080fd5b813567ffffffffffffffff8111156102ed57600080fd5b6102f9848285016101c8565b949350505050565b634e487b7160e01b600052603260045260246000fd' + +export const MockCustomEntitlement = { + abi: _abi, + bytecode: _bytecode, +} diff --git a/packages/web3/src/MockERC20.ts b/packages/web3/src/MockERC20.ts new file mode 100644 index 000000000..e64253905 --- /dev/null +++ b/packages/web3/src/MockERC20.ts @@ -0,0 +1,579 @@ +import type { Abi } from 'abitype' +import { Address } from './ContractTypes' + +const abi: Abi = [ + { + type: 'constructor', + inputs: [ + { + name: 'name', + type: 'string', + internalType: 'string', + }, + { + name: 'symbol', + type: 'string', + internalType: 'string', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'DOMAIN_SEPARATOR', + inputs: [], + outputs: [ + { + name: 'result', + type: 'bytes32', + internalType: 'bytes32', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: '__ERC20_init', + inputs: [ + { + name: 'name_', + type: 'string', + internalType: 'string', + }, + { + name: 'symbol_', + type: 'string', + internalType: 'string', + }, + { + name: 'decimals_', + type: 'uint8', + internalType: 'uint8', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: '__Introspection_init', + inputs: [], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'allowance', + inputs: [ + { + name: 'owner', + type: 'address', + internalType: 'address', + }, + { + name: 'spender', + type: 'address', + internalType: 'address', + }, + ], + outputs: [ + { + name: 'result', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'approve', + inputs: [ + { + name: 'spender', + type: 'address', + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [ + { + name: '', + type: 'bool', + internalType: 'bool', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'balanceOf', + inputs: [ + { + name: 'account', + type: 'address', + internalType: 'address', + }, + ], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'decimals', + inputs: [], + outputs: [ + { + name: '', + type: 'uint8', + internalType: 'uint8', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'eip712Domain', + inputs: [], + outputs: [ + { + name: 'fields', + type: 'bytes1', + internalType: 'bytes1', + }, + { + name: 'name', + type: 'string', + internalType: 'string', + }, + { + name: 'version', + type: 'string', + internalType: 'string', + }, + { + name: 'chainId', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'verifyingContract', + type: 'address', + internalType: 'address', + }, + { + name: 'salt', + type: 'bytes32', + internalType: 'bytes32', + }, + { + name: 'extensions', + type: 'uint256[]', + internalType: 'uint256[]', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'mint', + inputs: [ + { + name: 'account', + type: 'address', + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'name', + inputs: [], + outputs: [ + { + name: '', + type: 'string', + internalType: 'string', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'nonces', + inputs: [ + { + name: 'owner', + type: 'address', + internalType: 'address', + }, + ], + outputs: [ + { + name: 'result', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'permit', + inputs: [ + { + name: 'owner', + type: 'address', + internalType: 'address', + }, + { + name: 'spender', + type: 'address', + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'deadline', + type: 'uint256', + internalType: 'uint256', + }, + { + name: 'v', + type: 'uint8', + internalType: 'uint8', + }, + { + name: 'r', + type: 'bytes32', + internalType: 'bytes32', + }, + { + name: 's', + type: 'bytes32', + internalType: 'bytes32', + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'supportsInterface', + inputs: [ + { + name: 'interfaceId', + type: 'bytes4', + internalType: 'bytes4', + }, + ], + outputs: [ + { + name: '', + type: 'bool', + internalType: 'bool', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'symbol', + inputs: [], + outputs: [ + { + name: '', + type: 'string', + internalType: 'string', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'totalSupply', + inputs: [], + outputs: [ + { + name: '', + type: 'uint256', + internalType: 'uint256', + }, + ], + stateMutability: 'view', + }, + { + type: 'function', + name: 'transfer', + inputs: [ + { + name: 'to', + type: 'address', + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [ + { + name: '', + type: 'bool', + internalType: 'bool', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'function', + name: 'transferFrom', + inputs: [ + { + name: 'from', + type: 'address', + internalType: 'address', + }, + { + name: 'to', + type: 'address', + internalType: 'address', + }, + { + name: 'amount', + type: 'uint256', + internalType: 'uint256', + }, + ], + outputs: [ + { + name: '', + type: 'bool', + internalType: 'bool', + }, + ], + stateMutability: 'nonpayable', + }, + { + type: 'event', + name: 'Approval', + inputs: [ + { + name: 'owner', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'spender', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'EIP712DomainChanged', + inputs: [], + anonymous: false, + }, + { + type: 'event', + name: 'Initialized', + inputs: [ + { + name: 'version', + type: 'uint32', + indexed: false, + internalType: 'uint32', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'InterfaceAdded', + inputs: [ + { + name: 'interfaceId', + type: 'bytes4', + indexed: true, + internalType: 'bytes4', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'InterfaceRemoved', + inputs: [ + { + name: 'interfaceId', + type: 'bytes4', + indexed: true, + internalType: 'bytes4', + }, + ], + anonymous: false, + }, + { + type: 'event', + name: 'Transfer', + inputs: [ + { + name: 'from', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'to', + type: 'address', + indexed: true, + internalType: 'address', + }, + { + name: 'value', + type: 'uint256', + indexed: false, + internalType: 'uint256', + }, + ], + anonymous: false, + }, + { + type: 'error', + name: 'AllowanceOverflow', + inputs: [], + }, + { + type: 'error', + name: 'AllowanceUnderflow', + inputs: [], + }, + { + type: 'error', + name: 'ECDSAInvalidSignature', + inputs: [], + }, + { + type: 'error', + name: 'ECDSAInvalidSignatureLength', + inputs: [ + { + name: 'length', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + { + type: 'error', + name: 'ECDSAInvalidSignatureS', + inputs: [ + { + name: 's', + type: 'bytes32', + internalType: 'bytes32', + }, + ], + }, + { + type: 'error', + name: 'Initializable_InInitializingState', + inputs: [], + }, + { + type: 'error', + name: 'Initializable_NotInInitializingState', + inputs: [], + }, + { + type: 'error', + name: 'InsufficientAllowance', + inputs: [], + }, + { + type: 'error', + name: 'InsufficientBalance', + inputs: [], + }, + { + type: 'error', + name: 'Introspection_AlreadySupported', + inputs: [], + }, + { + type: 'error', + name: 'Introspection_NotSupported', + inputs: [], + }, + { + type: 'error', + name: 'InvalidAccountNonce', + inputs: [ + { + name: 'account', + type: 'address', + internalType: 'address', + }, + { + name: 'currentNonce', + type: 'uint256', + internalType: 'uint256', + }, + ], + }, + { + type: 'error', + name: 'InvalidPermit', + inputs: [], + }, + { + type: 'error', + name: 'PermitExpired', + inputs: [], + }, + { + type: 'error', + name: 'TotalSupplyOverflow', + inputs: [], + }, +] + +const bytecode: Address = + '0x60806040523480156200001157600080fd5b5060405162001a7838038062001a78833981016040819052620000349162000418565b6200003e62000054565b6200004c82826012620000fc565b5050620005de565b7f59b501c3653afc186af7d48dda36cf6732bd21629a6295693664240a6ef520008054640100000000900460ff1615620000a1576040516366008a2d60e01b815260040160405180910390fd5b805463ffffffff9081161015620000f957805463ffffffff191663ffffffff90811782556040519081527fe9c9b456cb2994b80aeef036cf59d26e9617df80f816a6ee5a5b4166e07e2f5c9060200160405180910390a15b50565b6200010e6336372b0760e01b6200016f565b62000120634ec7fbed60e11b6200016f565b6200013263a219a02560e01b6200016f565b6200013f8383836200024f565b6200016a83604051806040016040528060018152602001603160f81b815250620002a860201b60201c565b505050565b6001600160e01b0319811660009081527f81088bbc801e045ea3e7620779ab349988f58afbdfba10dff983df3f33522b00602052604090205460ff161515600114620001fe576001600160e01b0319811660009081527f81088bbc801e045ea3e7620779ab349988f58afbdfba10dff983df3f33522b0060205260409020805460ff1916600117905562000217565b604051637967f77d60e11b815260040160405180910390fd5b6040516001600160e01b03198216907f78f84e5b1c5c05be2b5ad3800781dd404d6d6c6302bc755c0fe20f58a33a7f2290600090a250565b7f42eeb43a78e08448a75e4dd4bca52199850157e8648ba508b0c6a00addcdffbe806200027d858262000512565b50600181016200028e848262000512565b50600201805460ff191660ff929092169190911790555050565b7f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336e620002d5838262000512565b507f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336f62000303828262000512565b505060007f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336c8190557f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336d5550565b634e487b7160e01b600052604160045260246000fd5b600082601f8301126200037857600080fd5b81516001600160401b038082111562000395576200039562000350565b604051601f8301601f19908116603f01168101908282118183101715620003c057620003c062000350565b8160405283815260209250866020858801011115620003de57600080fd5b600091505b83821015620004025785820183015181830184015290820190620003e3565b6000602085830101528094505050505092915050565b600080604083850312156200042c57600080fd5b82516001600160401b03808211156200044457600080fd5b620004528683870162000366565b935060208501519150808211156200046957600080fd5b50620004788582860162000366565b9150509250929050565b600181811c908216806200049757607f821691505b602082108103620004b857634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156200016a576000816000526020600020601f850160051c81016020861015620004e95750805b601f850160051c820191505b818110156200050a57828155600101620004f5565b505050505050565b81516001600160401b038111156200052e576200052e62000350565b62000546816200053f845462000482565b84620004be565b602080601f8311600181146200057e5760008415620005655750858301515b600019600386901b1c1916600185901b1785556200050a565b600085815260208120601f198616915b82811015620005af578886015182559484019460019091019084016200058e565b5085821015620005ce5787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b61148a80620005ee6000396000f3fe608060405234801561001057600080fd5b506004361061010b5760003560e01c806370a08231116100a257806395d89b411161007157806395d89b4114610209578063a9059cbb14610211578063aa23aa0214610224578063d505accf14610237578063dd62ed3e1461024a57600080fd5b806370a08231146101c05780637ecebe00146101d357806384b0196e146101e6578063930fc8ca1461020157600080fd5b806323b872dd116100de57806323b872dd14610176578063313ce567146101895780633644e515146101a357806340c10f19146101ab57600080fd5b806301ffc9a71461011057806306fdde0314610138578063095ea7b31461014d57806318160ddd14610160575b600080fd5b61012361011e366004610f8b565b61025d565b60405190151581526020015b60405180910390f35b61014061026e565b60405161012f9190610ffb565b61012361015b36600461102a565b61027d565b610168610290565b60405190815260200161012f565b610123610184366004611054565b6102a3565b6101916102b8565b60405160ff909116815260200161012f565b6101686102e5565b6101be6101b936600461102a565b6102ef565b005b6101686101ce366004611090565b6102fd565b6101686101e1366004611090565b610317565b6101ee610354565b60405161012f97969594939291906110ab565b6101be61041c565b610140610470565b61012361021f36600461102a565b61047a565b6101be6102323660046111f8565b610486565b6101be61024536600461126c565b6104e0565b6101686102583660046112d6565b6104f8565b600061026882610517565b92915050565b606061027861055a565b905090565b6000610289838361060b565b9392505050565b60006102786805345cdf77eb68f44c5490565b60006102b084848461065e565b949350505050565b60006102787f42eeb43a78e08448a75e4dd4bca52199850157e8648ba508b0c6a00addcdffc05460ff1690565b600061027861071c565b6102f98282610726565b5050565b6387a211a2600c9081526000828152602090912054610268565b6001600160a01b03811660009081527fda5d6d87446d81938877f0ee239dac391146dd7466ea30567f72becf06773c006020526040812054610268565b60006060808280808360008051602061146a8339815191525415801561039957507f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336d54155b6103e25760405162461bcd60e51b81526020600482015260156024820152741152540dcc4c8e88155b9a5b9a5d1a585b1a5e9959605a1b60448201526064015b60405180910390fd5b6103ea6107a5565b6103f26107c4565b60408051600080825260208201909252600f60f81b9b939a50919850469750309650945092509050565b7f59b501c3653afc186af7d48dda36cf6732bd21629a6295693664240a6ef5200054640100000000900460ff1661046657604051630ef4733760e31b815260040160405180910390fd5b61046e6107e3565b565b60606102786107f3565b60006102898383610824565b7f59b501c3653afc186af7d48dda36cf6732bd21629a6295693664240a6ef5200054640100000000900460ff166104d057604051630ef4733760e31b815260040160405180910390fd5b6104db83838361089f565b505050565b6104ef878787878787876108fd565b50505050505050565b6020819052637f5e9f20600c9081526000838152603490912054610289565b6001600160e01b03191660009081527f81088bbc801e045ea3e7620779ab349988f58afbdfba10dff983df3f33522b00602052604090205460ff16151560011490565b60607f42eeb43a78e08448a75e4dd4bca52199850157e8648ba508b0c6a00addcdffbe805461058890611309565b80601f01602080910402602001604051908101604052809291908181526020018280546105b490611309565b80156106015780601f106105d657610100808354040283529160200191610601565b820191906000526020600020905b8154815290600101906020018083116105e457829003601f168201915b5050505050905090565b600082602052637f5e9f20600c5233600052816034600c205581600052602c5160601c337f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560206000a350600192915050565b60008360601b33602052637f5e9f208117600c526034600c208054600181011561069e5780851115610698576313be252b6000526004601cfd5b84810382555b50506387a211a28117600c526020600c208054808511156106c75763f4d678b86000526004601cfd5b84810382555050836000526020600c208381540181555082602052600c5160601c8160601c7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602080a3505060019392505050565b6000610278610a9e565b6805345cdf77eb68f44c548181018181101561074a5763e5cfe9576000526004601cfd5b806805345cdf77eb68f44c5550506387a211a2600c52816000526020600c208181540181555080602052600c5160601c60007fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602080a35050565b606060008051602061146a833981519152600201805461058890611309565b606060008051602061146a833981519152600301805461058890611309565b61046e6301ffc9a760e01b610b12565b60607f42eeb43a78e08448a75e4dd4bca52199850157e8648ba508b0c6a00addcdffbe600101805461058890611309565b60006387a211a2600c52336000526020600c2080548084111561084f5763f4d678b86000526004601cfd5b83810382555050826000526020600c208281540181555081602052600c5160601c337fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef602080a350600192915050565b6108af6336372b0760e01b610b12565b6108bf634ec7fbed60e11b610b12565b6108cf63a219a02560e01b610b12565b6108da838383610bb8565b6104db83604051806040016040528060018152602001603160f81b815250610c0d565b8342111561094d5760405162461bcd60e51b815260206004820152601d60248201527f45524332305065726d69743a206578706972656420646561646c696e6500000060448201526064016103d9565b60007f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886109b9836001600160a01b031660009081527fda5d6d87446d81938877f0ee239dac391146dd7466ea30567f72becf06773c006020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090506000610a1482610c9f565b90506000610a2482878787610ccc565b9050896001600160a01b0316816001600160a01b031614610a875760405162461bcd60e51b815260206004820152601e60248201527f45524332305065726d69743a20696e76616c6964207369676e6174757265000060448201526064016103d9565b610a928a8a8a610cfa565b50505050505050505050565b60007f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f610ac9610d4d565b610ad1610db3565b60408051602081019490945283019190915260608201524660808201523060a082015260c00160405160208183030381529060405280519060200120905090565b610b1b81610517565b610b67576001600160e01b0319811660009081527f81088bbc801e045ea3e7620779ab349988f58afbdfba10dff983df3f33522b0060205260409020805460ff19166001179055610b80565b604051637967f77d60e11b815260040160405180910390fd5b6040516001600160e01b03198216907f78f84e5b1c5c05be2b5ad3800781dd404d6d6c6302bc755c0fe20f58a33a7f2290600090a250565b7f42eeb43a78e08448a75e4dd4bca52199850157e8648ba508b0c6a00addcdffbe80610be48582611393565b5060018101610bf38482611393565b50600201805460ff191660ff929092169190911790555050565b7f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336e610c388382611393565b507f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336f610c648282611393565b5050600060008051602061146a8339815191528190557f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336d5550565b6000610268610cac61071c565b8360405161190160f01b8152600281019290925260228201526042902090565b600080600080610cde88888888610e03565b925092509250610cee8282610ed2565b50909695505050505050565b8260601b82602052637f5e9f208117600c52816034600c205581600052602c5160601c8160601c7f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92560206000a350505050565b600080610d586107a5565b805190915015610d6f578051602090910120919050565b60008051602061146a833981519152548015610d8b5792915050565b7fc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a4709250505090565b600080610dbe6107c4565b805190915015610dd5578051602090910120919050565b7f3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336d548015610d8b5792915050565b600080807f7fffffffffffffffffffffffffffffff5d576e7357a4501ddfe92f46681b20a0841115610e3e5750600091506003905082610ec8565b604080516000808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015610e92573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b038116610ebe57506000925060019150829050610ec8565b9250600091508190505b9450945094915050565b6000826003811115610ee657610ee6611453565b03610eef575050565b6001826003811115610f0357610f03611453565b03610f215760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115610f3557610f35611453565b03610f565760405163fce698f760e01b8152600481018290526024016103d9565b6003826003811115610f6a57610f6a611453565b036102f9576040516335e2f38360e21b8152600481018290526024016103d9565b600060208284031215610f9d57600080fd5b81356001600160e01b03198116811461028957600080fd5b6000815180845260005b81811015610fdb57602081850181015186830182015201610fbf565b506000602082860101526020601f19601f83011685010191505092915050565b6020815260006102896020830184610fb5565b80356001600160a01b038116811461102557600080fd5b919050565b6000806040838503121561103d57600080fd5b6110468361100e565b946020939093013593505050565b60008060006060848603121561106957600080fd5b6110728461100e565b92506110806020850161100e565b9150604084013590509250925092565b6000602082840312156110a257600080fd5b6102898261100e565b60ff60f81b881681526000602060e060208401526110cc60e084018a610fb5565b83810360408501526110de818a610fb5565b606085018990526001600160a01b038816608086015260a0850187905284810360c08601528551808252602080880193509091019060005b8181101561113257835183529284019291840191600101611116565b50909c9b505050505050505050505050565b634e487b7160e01b600052604160045260246000fd5b600082601f83011261116b57600080fd5b813567ffffffffffffffff8082111561118657611186611144565b604051601f8301601f19908116603f011681019082821181831017156111ae576111ae611144565b816040528381528660208588010111156111c757600080fd5b836020870160208301376000602085830101528094505050505092915050565b803560ff8116811461102557600080fd5b60008060006060848603121561120d57600080fd5b833567ffffffffffffffff8082111561122557600080fd5b6112318783880161115a565b9450602086013591508082111561124757600080fd5b506112548682870161115a565b925050611263604085016111e7565b90509250925092565b600080600080600080600060e0888a03121561128757600080fd5b6112908861100e565b965061129e6020890161100e565b955060408801359450606088013593506112ba608089016111e7565b925060a0880135915060c0880135905092959891949750929550565b600080604083850312156112e957600080fd5b6112f28361100e565b91506113006020840161100e565b90509250929050565b600181811c9082168061131d57607f821691505b60208210810361133d57634e487b7160e01b600052602260045260246000fd5b50919050565b601f8211156104db576000816000526020600020601f850160051c8101602086101561136c5750805b601f850160051c820191505b8181101561138b57828155600101611378565b505050505050565b815167ffffffffffffffff8111156113ad576113ad611144565b6113c1816113bb8454611309565b84611343565b602080601f8311600181146113f657600084156113de5750858301515b600019600386901b1c1916600185901b17855561138b565b600085815260208120601f198616915b8281101561142557888601518255948401946001909101908401611406565b50858210156114435787850151600019600388901b60f8161c191681555b5050505050600190811b01905550565b634e487b7160e01b600052602160045260246000fdfe3a497e775dc7c283402f0d3c39c5f0ea53870eb15ab2dddfde5a1162a84c336c' + +export const MockERC20 = { + abi, + bytecode, +} diff --git a/packages/web3/src/TestCustomEntitlement.ts b/packages/web3/src/TestCustomEntitlement.ts new file mode 100644 index 000000000..692fc1ae3 --- /dev/null +++ b/packages/web3/src/TestCustomEntitlement.ts @@ -0,0 +1,115 @@ +import { createTestClient, http, publicActions, walletActions, parseEther } from 'viem' +import { foundry } from 'viem/chains' +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' + +import { MockCustomEntitlement } from './MockCustomEntitlement' +import { deployContract, Mutex } from './TestGatingUtils' +import { Address } from './ContractTypes' + +import { dlogger } from '@river-build/dlog' + +const logger = dlogger('csb:TestGatingERC20') + +const mockCustomContracts = new Map() +const mockCustomContractsMutex = new Mutex() + +async function getContractAddress(tokenName: string): Promise
{ + try { + await mockCustomContractsMutex.lock() + if (!mockCustomContracts.has(tokenName)) { + const contractAddress = await deployContract( + tokenName, + MockCustomEntitlement.abi, + MockCustomEntitlement.bytecode, + ) + mockCustomContracts.set(tokenName, contractAddress) + } + } catch (e) { + logger.error('Failed to deploy contract', e) + throw new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Failed to get contract address: ${tokenName}`, + ) + } finally { + mockCustomContractsMutex.unlock() + } + + return mockCustomContracts.get(tokenName)! +} + +async function setEntitled( + customEntitlementContractName: string, + userAddresses: Address[], + entitled: boolean, +): Promise { + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) + const client = createTestClient({ + chain: foundry, + mode: 'anvil', + transport: http(), + account: throwawayAccount, + }) + .extend(publicActions) + .extend(walletActions) + + await client.setBalance({ + address: throwawayAccount.address, + value: parseEther('1'), + }) + + const contractAddress = await getContractAddress(customEntitlementContractName) + + logger.log( + `Setting custom entitlement to ${entitled} for users ${userAddresses.join( + ',', + )} for contract ${customEntitlementContractName}`, + ) + const txnReceipt = await client.writeContract({ + address: contractAddress, + abi: MockCustomEntitlement.abi, + functionName: 'setEntitled', + args: [userAddresses, entitled], + account: throwawayAccount, + }) + + const receipt = await client.waitForTransactionReceipt({ hash: txnReceipt }) + expect(receipt.status).toBe('success') + logger.log( + `Set custom entitlement to ${entitled} for users ${userAddresses.join( + ',', + )} for contract ${customEntitlementContractName}`, + ) +} + +async function isEntitled( + customEntitlementContractName: string, + userAddresses: Address[], +): Promise { + const contractAddress = await getContractAddress(customEntitlementContractName) + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) + const client = createTestClient({ + chain: foundry, + mode: 'anvil', + transport: http(), + account: throwawayAccount, + }) + .extend(publicActions) + .extend(walletActions) + + const result = await client.readContract({ + address: contractAddress, + abi: MockCustomEntitlement.abi, + functionName: 'isEntitled', + args: [userAddresses], + }) + + return result as boolean +} + +export const TestCustomEntitlement = { + getContractAddress, + setEntitled, + isEntitled, +} diff --git a/packages/web3/src/TestGatingERC20.ts b/packages/web3/src/TestGatingERC20.ts new file mode 100644 index 000000000..6f10a56bf --- /dev/null +++ b/packages/web3/src/TestGatingERC20.ts @@ -0,0 +1,148 @@ +import { createTestClient, http, publicActions, walletActions, parseEther } from 'viem' +import { foundry } from 'viem/chains' +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' + +import { MockERC20 } from './MockERC20' +import { deployContract, Mutex } from './TestGatingUtils' +import { Address } from './ContractTypes' +import { dlogger } from '@river-build/dlog' + +const logger = dlogger('csb:TestGatingERC20') + +const erc20Contracts = new Map() +const erc20ContractsMutex = new Mutex() + +async function getContractAddress(tokenName: string): Promise
{ + try { + await erc20ContractsMutex.lock() + if (!erc20Contracts.has(tokenName)) { + const contractAddress = await deployContract( + tokenName, + MockERC20.abi, + MockERC20.bytecode, + ['TestERC20', 'TST'], + ) + erc20Contracts.set(tokenName, contractAddress) + } + } catch (e) { + logger.error('Failed to deploy contract', e) + throw new Error( + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + `Failed to get contract address: ${tokenName}`, + ) + } finally { + erc20ContractsMutex.unlock() + } + + return erc20Contracts.get(tokenName)! +} + +async function publicMint(tokenName: string, toAddress: Address, amount: number): Promise { + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) + const client = createTestClient({ + chain: foundry, + mode: 'anvil', + transport: http(), + account: throwawayAccount, + }) + .extend(publicActions) + .extend(walletActions) + + await client.setBalance({ + address: throwawayAccount.address, + value: parseEther('1'), + }) + + const contractAddress = await getContractAddress(tokenName) + + logger.log('minting', contractAddress, toAddress) + + const nftReceipt = await client.writeContract({ + address: contractAddress, + abi: MockERC20.abi, + functionName: 'mint', + args: [toAddress, amount], + account: throwawayAccount, + }) + + logger.log('minted', nftReceipt) + + const receipt = await client.waitForTransactionReceipt({ hash: nftReceipt }) + expect(receipt.status).toBe('success') + + // create a filter to listen for the Transfer event to find the token id + // don't worry about the possibility of non-matching arguments, as we're specifying the contract + // address of the contract we're interested in. + const filter = await client.createContractEventFilter({ + abi: MockERC20.abi, + address: contractAddress, + eventName: 'Transfer', + args: { + to: toAddress, + }, + fromBlock: receipt.blockNumber, + toBlock: receipt.blockNumber, + }) + const eventLogs = await client.getFilterLogs({ filter }) + for (const eventLog of eventLogs) { + if (eventLog.transactionHash === receipt.transactionHash) { + logger.log('mint logs', eventLog.args) + return + } + } + + throw Error('No mint event found') +} + +async function totalSupply(contractName: string): Promise { + const contractAddress = await getContractAddress(contractName) + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) + const client = createTestClient({ + chain: foundry, + mode: 'anvil', + transport: http(), + account: throwawayAccount, + }) + .extend(publicActions) + .extend(walletActions) + + const totalSupply = await client.readContract({ + address: contractAddress, + abi: MockERC20.abi, + functionName: 'totalSupply', + args: [], + }) + return Number(totalSupply) +} + +async function balanceOf(contractName: string, address: Address): Promise { + const contractAddress = await getContractAddress(contractName) + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) + const client = createTestClient({ + chain: foundry, + mode: 'anvil', + transport: http(), + account: throwawayAccount, + }) + .extend(publicActions) + .extend(walletActions) + + const balance = await client.readContract({ + address: contractAddress, + abi: MockERC20.abi, + functionName: 'balanceOf', + args: [address], + }) + + return Number(balance) +} + +export const TestERC20 = { + getContractAddress, + balanceOf, + totalSupply, + publicMint, +} diff --git a/packages/web3/src/TestGatingNFT.ts b/packages/web3/src/TestGatingNFT.ts index 7dc8a9463..afa1234b6 100644 --- a/packages/web3/src/TestGatingNFT.ts +++ b/packages/web3/src/TestGatingNFT.ts @@ -1,44 +1,16 @@ -import { createTestClient, http, publicActions, walletActions } from 'viem' +import { createTestClient, http, publicActions, walletActions, parseEther } from 'viem' +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' import { foundry } from 'viem/chains' import MockERC721a from './MockERC721A' -import { keccak256, parseEther } from 'viem/utils' -import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts' - +import { isHexString, deployContract, Mutex } from './TestGatingUtils' import { Address } from './ContractTypes' import { dlogger } from '@river-build/dlog' const logger = dlogger('csb:TestGatingNFT') -export function toEIP55Address(address: Address): Address { - const addressHash = keccak256(address.substring(2).toLowerCase() as Address) - let checksumAddress = '0x' - - for (let i = 2; i < address.length; i++) { - if (parseInt(addressHash[i], 16) >= 8) { - checksumAddress += address[i].toUpperCase() - } else { - checksumAddress += address[i].toLowerCase() - } - } - - return checksumAddress as Address -} - -export function isEIP55Address(address: Address): boolean { - return address === toEIP55Address(address) -} -/* - */ -export function isHexString(value: unknown): value is Address { - // Check if the value is undefined first - if (value === undefined) { - return false - } - return typeof value === 'string' && /^0x[0-9a-fA-F]+$/.test(value) -} export class TestGatingNFT { public async publicMint(toAddress: string) { if (!isHexString(toAddress)) { @@ -49,133 +21,38 @@ export class TestGatingNFT { } } -class Mutex { - queue: ((value: void | PromiseLike) => void)[] - locked: boolean - constructor() { - this.queue = [] - this.locked = false - } - - lock() { - if (!this.locked) { - this.locked = true - return Promise.resolve() - } - - let unlockNext: (value: void | PromiseLike) => void - - const promise = new Promise((resolve) => { - unlockNext = resolve - }) - - this.queue.push(unlockNext!) - - return promise - } - - unlock() { - if (this.queue.length > 0) { - const unlockNext = this.queue.shift() - unlockNext?.() - } else { - this.locked = false - } - } -} - const nftContracts = new Map() const nftContractsMutex = new Mutex() -export async function getContractAddress(nftName: string): Promise
{ - let retryCount = 0 - let lastError: unknown +async function getContractAddress(nftName: string): Promise
{ try { - // If mulitple callers are in a Promise.all() and they all try to deploy the same contract at the same time, - // we want to make sure that only one of them actually deploys the contract. await nftContractsMutex.lock() - if (!nftContracts.has(nftName)) { - while (retryCount++ < 5) { - try { - const privateKey = generatePrivateKey() - const throwawayAccount = privateKeyToAccount(privateKey) - const client = createTestClient({ - chain: foundry, - mode: 'anvil', - transport: http(), - account: throwawayAccount, - }) - .extend(publicActions) - .extend(walletActions) - - await client.setBalance({ - address: throwawayAccount.address, - value: parseEther('1'), - }) - - const hash = await client.deployContract({ - abi: MockERC721a.abi, - account: throwawayAccount, - bytecode: MockERC721a.bytecode.object, - }) - - const receipt = await client.waitForTransactionReceipt({ hash }) - - if (receipt.contractAddress) { - logger.info( - 'deployed', - nftName, - receipt.contractAddress, - isEIP55Address(receipt.contractAddress), - nftContracts, - ) - // For some reason the address isn't in EIP-55, so we need to checksum it - nftContracts.set(nftName, toEIP55Address(receipt.contractAddress)) - } else { - throw new Error('Failed to deploy contract') - } - break - } catch (e) { - lastError = e - if ( - typeof e === 'object' && - e !== null && - 'message' in e && - typeof e.message === 'string' && - (e.message.includes('nonce too low') || - e.message.includes('NonceTooLowError') || - e.message.includes( - 'Nonce provided for the transaction is lower than the current nonce', - )) - ) { - logger.log('retrying because nonce too low', e, retryCount) - } else { - throw e - } - } - } + const contractAddress = await deployContract( + nftName, + MockERC721a.abi, + MockERC721a.bytecode.object, + ) + nftContracts.set(nftName, contractAddress) } - } finally { - nftContractsMutex.unlock() - } - - const contractAddress = nftContracts.get(nftName) - if (!contractAddress) { + } catch (e) { + logger.error('Failed to deploy contract', e) throw new Error( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `Failed to get contract address: ${nftName} retryCount: ${retryCount} lastError: ${lastError} `, + `Failed to get contract address: ${nftName}`, ) + } finally { + nftContractsMutex.unlock() } - return contractAddress + return nftContracts.get(nftName)! } export async function getTestGatingNFTContractAddress(): Promise
{ return await getContractAddress('TestGatingNFT') } -export async function publicMint(nftName: string, toAddress: Address): Promise { +async function publicMint(nftName: string, toAddress: Address): Promise { const privateKey = generatePrivateKey() const throwawayAccount = privateKeyToAccount(privateKey) const client = createTestClient({ @@ -233,36 +110,43 @@ export async function publicMint(nftName: string, toAddress: Address): Promise { +async function burn(nftName: string, tokenId: number): Promise { + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) const client = createTestClient({ chain: foundry, mode: 'anvil', transport: http(), + account: throwawayAccount, }) .extend(publicActions) .extend(walletActions) - const contractAddress = await getContractAddress(nftName) - - const account = (await client.getAddresses())[0] + await client.setBalance({ + address: throwawayAccount.address, + value: parseEther('1'), + }) const nftReceipt = await client.writeContract({ - address: contractAddress, + address: await getContractAddress(nftName), abi: MockERC721a.abi, functionName: 'burn', args: [BigInt(tokenId)], - account, + account: throwawayAccount, }) const receipt = await client.waitForTransactionReceipt({ hash: nftReceipt }) expect(receipt.status).toBe('success') } -export async function balanceOf(nftName: string, address: Address): Promise { +async function balanceOf(nftName: string, address: Address): Promise { + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) const client = createTestClient({ chain: foundry, mode: 'anvil', transport: http(), + account: throwawayAccount, }) .extend(publicActions) .extend(walletActions) @@ -278,3 +162,10 @@ export async function balanceOf(nftName: string, address: Address): Promise) => void)[] + locked: boolean + constructor() { + this.queue = [] + this.locked = false + } + + lock() { + if (!this.locked) { + this.locked = true + return Promise.resolve() + } + + let unlockNext: (value: void | PromiseLike) => void + + const promise = new Promise((resolve) => { + unlockNext = resolve + }) + + this.queue.push(unlockNext!) + + return promise + } + + unlock() { + if (this.queue.length > 0) { + const unlockNext = this.queue.shift() + unlockNext?.() + } else { + this.locked = false + } + } +} + +export function toEIP55Address(address: Address): Address { + const addressHash = keccak256(address.substring(2).toLowerCase() as Address) + let checksumAddress = '0x' + + for (let i = 2; i < address.length; i++) { + if (parseInt(addressHash[i], 16) >= 8) { + checksumAddress += address[i].toUpperCase() + } else { + checksumAddress += address[i].toLowerCase() + } + } + + return checksumAddress as Address +} + +export function isEIP55Address(address: Address): boolean { + return address === toEIP55Address(address) +} + +export function isHexString(value: unknown): value is Address { + // Check if the value is undefined first + if (value === undefined) { + return false + } + return typeof value === 'string' && /^0x[0-9a-fA-F]+$/.test(value) +} + +export async function deployContract( + contractName: string, + abi: Abi, + bytecode: Address, // bytecode is a hex string + constructorArgs: any[] = [], +): Promise
{ + let retryCount = 0 + let lastError: unknown + while (retryCount++ < 5) { + try { + const privateKey = generatePrivateKey() + const throwawayAccount = privateKeyToAccount(privateKey) + const client = createTestClient({ + chain: foundry, + mode: 'anvil', + transport: http(), + account: throwawayAccount, + }) + .extend(publicActions) + .extend(walletActions) + + await client.setBalance({ + address: throwawayAccount.address, + value: parseEther('1'), + }) + + const hash = await client.deployContract({ + abi, + account: throwawayAccount, + args: constructorArgs, + bytecode, + }) + + const receipt = await client.waitForTransactionReceipt({ hash }) + + if (receipt.contractAddress) { + logger.info( + 'deployed', + receipt.contractAddress, + isEIP55Address(receipt.contractAddress), + ) + return toEIP55Address(receipt.contractAddress) + } else { + throw new Error(`Failed to deploy contract ${contractName}`) + } + } catch (e) { + lastError = e + if ( + typeof e === 'object' && + e !== null && + 'message' in e && + typeof e.message === 'string' && + (e.message.includes('nonce too low') || + e.message.includes('NonceTooLowError') || + e.message.includes( + 'Nonce provided for the transaction is lower than the current nonce', + )) + ) { + logger.log('retrying because nonce too low', e, retryCount, contractName) + } else { + throw e + } + } + } + throw lastError +} diff --git a/packages/web3/src/entitlement.ts b/packages/web3/src/entitlement.ts index 275ba8184..15d80b296 100644 --- a/packages/web3/src/entitlement.ts +++ b/packages/web3/src/entitlement.ts @@ -446,8 +446,16 @@ async function evaluateCheckOperation( case CheckOperationType.MOCK: { return evaluateMockOperation(operation, controller) } - case CheckOperationType.ISENTITLED: - throw new Error(`CheckOperationType.ISENTITLED not implemented`) + case CheckOperationType.ISENTITLED: { + await Promise.all(providers.map((p) => p.ready)) + const provider = findProviderFromChainId(providers, operation.chainId) + + if (!provider) { + controller.abort() + return zeroAddress + } + return evaluateCustomEntitledOperation(operation, controller, provider, linkedWallets) + } case CheckOperationType.ERC20: { await Promise.all(providers.map((p) => p.ready)) const provider = findProviderFromChainId(providers, operation.chainId) @@ -685,8 +693,33 @@ async function evaluateERC20Operation( ) } +async function evaluateCustomEntitledOperation( + operation: CheckOperation, + controller: AbortController, + provider: ethers.providers.StaticJsonRpcProvider, + linkedWallets: string[], +): Promise { + const contract = new ethers.Contract( + operation.contractAddress, + ['function isEntitled(address[]) view returns (bool)'], + provider, + ) + return await Promise.any( + linkedWallets.map(async (wallet): Promise
=> { + const isEntitled = await contract.callStatic.isEntitled([wallet]) + if (isEntitled === true) { + return wallet as Address + } + throw new Error('Not entitled') + }), + ).catch(() => { + controller.abort() + return zeroAddress + }) +} + async function evaluateContractBalanceAcrossWallets( - contractAddress: `0x${string}`, + contractAddress: Address, threshold: bigint, controller: AbortController, provider: ethers.providers.StaticJsonRpcProvider, diff --git a/packages/web3/src/index.ts b/packages/web3/src/index.ts index f49c268a7..f1951efe4 100644 --- a/packages/web3/src/index.ts +++ b/packages/web3/src/index.ts @@ -3,6 +3,8 @@ export * from './ISpaceDapp' export * from './ContractTypes' export * from './LocalhostWeb3Provider' export * from './TestGatingNFT' +export * from './TestGatingERC20' +export * from './TestCustomEntitlement' export * from './SpaceDappFactory' export * from './Utils' export * from './Web3Constants'