mirror of
https://github.com/lens-protocol/core.git
synced 2026-01-15 00:48:12 -05:00
Merge pull request #86 from aave/feat/peripheral-profile-metadata
This commit is contained in:
@@ -342,6 +342,21 @@ library DataTypes {
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `setProfileMetadataWithSig()` function.
|
||||
*
|
||||
* @param user The user which is the message signer.
|
||||
* @param profileId The profile ID for which to set the metadata.
|
||||
* @param metadata The metadata string to set for the profile and user.
|
||||
* @param sig The EIP712Signature struct containing the user's signature.
|
||||
*/
|
||||
struct SetProfileMetadataWithSigData {
|
||||
address user;
|
||||
uint256 profileId;
|
||||
string metadata;
|
||||
EIP712Signature sig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice A struct containing the parameters required for the `toggleFollowWithSig()` function.
|
||||
*
|
||||
|
||||
@@ -20,6 +20,7 @@ library Errors {
|
||||
error ProfileCreatorNotWhitelisted();
|
||||
error NotProfileOwner();
|
||||
error NotProfileOwnerOrDispatcher();
|
||||
error NotDispatcher();
|
||||
error PublicationDoesNotExist();
|
||||
error HandleTaken();
|
||||
error HandleLengthInvalid();
|
||||
|
||||
@@ -477,7 +477,7 @@ library Events {
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when the user wants to enable or disable the follow.
|
||||
* @dev Emitted when the user wants to enable or disable follows in the `LensPeripheryDataProvider`.
|
||||
*
|
||||
* @param owner The profile owner who executed the toggle.
|
||||
* @param profileIds The array of token IDs of the profiles each followNFT is associated with.
|
||||
@@ -490,4 +490,19 @@ library Events {
|
||||
bool[] enabled,
|
||||
uint256 timestamp
|
||||
);
|
||||
|
||||
/**
|
||||
* @dev Emitted when the metadata associated with a profile and user is set in the `LensPeripheryDataProvider`.
|
||||
*
|
||||
* @param user The user the metadata is set for.
|
||||
* @param profileId The profile ID the metadata is set for.
|
||||
* @param metadata The metadata set for the profile and user.
|
||||
* @param timestamp The current block timestamp.
|
||||
*/
|
||||
event ProfileMetadataSet(
|
||||
address indexed user,
|
||||
uint256 indexed profileId,
|
||||
string metadata,
|
||||
uint256 timestamp
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import {Events} from '../libraries/Events.sol';
|
||||
import {Errors} from '../libraries/Errors.sol';
|
||||
|
||||
/**
|
||||
* @notice This is a peripheral contract that allows for users to emit an event demonstrating whether or not
|
||||
* they explicitly want a follow to be shown.
|
||||
* @notice This is a peripheral contract that acts as a source of truth for profile metadata and allows
|
||||
* for users to emit an event demonstrating whether or not they explicitly want a follow to be shown.
|
||||
*
|
||||
* @dev This is useful because it allows clients to filter out follow NFTs that were transferred to
|
||||
* a recipient by another user (i.e. Not a mint) and not register them as "following" unless
|
||||
@@ -27,15 +27,70 @@ contract LensPeriphery {
|
||||
keccak256(
|
||||
'ToggleFollowWithSig(uint256[] profileIds,bool[] enables,uint256 nonce,uint256 deadline)'
|
||||
);
|
||||
bytes32 internal constant SET_PROFILE_METADATA_WITH_SIG_TYPEHASH =
|
||||
keccak256(
|
||||
'SetProfileMetadataURIWithSig(uint256 profileId,string metadata,uint256 nonce,uint256 deadline)'
|
||||
);
|
||||
|
||||
ILensHub immutable HUB;
|
||||
ILensHub public immutable HUB;
|
||||
|
||||
mapping(address => uint256) public sigNonces;
|
||||
|
||||
mapping(address => mapping(uint256 => string)) internal _metadataByProfileByOwner;
|
||||
|
||||
constructor(ILensHub hub) {
|
||||
HUB = hub;
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets profile metadata for a profile owner as a dispatcher.
|
||||
*
|
||||
* @param profileId The profile ID to set the metadata for.
|
||||
* @param metadata The metadata string to set for the profile and owner.
|
||||
*/
|
||||
function dispatcherSetProfileMetadataURI(uint256 profileId, string calldata metadata) external {
|
||||
address owner = IERC721Time(address(HUB)).ownerOf(profileId);
|
||||
if (msg.sender != HUB.getDispatcher(profileId)) revert Errors.NotDispatcher();
|
||||
_setProfileMetadataURI(owner, profileId, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the profile metadata for a given profile when owned by the message sender.
|
||||
*
|
||||
* @param profileId The profile ID to set the metadata for.
|
||||
* @param metadata The metadata string to set for the profile and message sender.
|
||||
*/
|
||||
function setProfileMetadataURI(uint256 profileId, string calldata metadata) external {
|
||||
_setProfileMetadataURI(msg.sender, profileId, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Sets the profile metadata for a given profile and user via signature with the specified parameters.
|
||||
*
|
||||
* @param vars A SetProfileMetadataWithSigData struct containingthe regular parameters as well as the user address
|
||||
* and an EIP712Signature struct.
|
||||
*/
|
||||
function setProfileMetadataURIWithSig(DataTypes.SetProfileMetadataWithSigData calldata vars)
|
||||
external
|
||||
{
|
||||
_validateRecoveredAddress(
|
||||
_calculateDigest(
|
||||
keccak256(
|
||||
abi.encode(
|
||||
SET_PROFILE_METADATA_WITH_SIG_TYPEHASH,
|
||||
vars.profileId,
|
||||
keccak256(bytes(vars.metadata)),
|
||||
sigNonces[vars.user]++,
|
||||
vars.sig.deadline
|
||||
)
|
||||
)
|
||||
),
|
||||
vars.user,
|
||||
vars.sig
|
||||
);
|
||||
_setProfileMetadataURI(vars.user, vars.profileId, vars.metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Toggle Follows on the given profiles, emiting toggle event for each FollowNFT.
|
||||
*
|
||||
@@ -73,6 +128,42 @@ contract LensPeriphery {
|
||||
_toggleFollow(vars.follower, vars.profileIds, vars.enables);
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the metadata URI of a profile for its current owner.
|
||||
*
|
||||
* @param profileId The profile ID to query the metadata URI for.
|
||||
*
|
||||
* @return string The metadata associated with that profile ID and the profile's current owner.
|
||||
*/
|
||||
function getProfileMetadataURI(uint256 profileId) external view returns (string memory) {
|
||||
address owner = IERC721Time(address(HUB)).ownerOf(profileId);
|
||||
return _metadataByProfileByOwner[owner][profileId];
|
||||
}
|
||||
|
||||
/**
|
||||
* @notice Returns the metadata URI of a profile for a given user. Note that the user does not *need* to own the
|
||||
* profile in order to have associated metadata.
|
||||
*
|
||||
* @param user The user to query the profile metadata URI for.
|
||||
* @param profileId The profile ID to query the metadata URI for.
|
||||
*/
|
||||
function getProfileMetadataURIByOwner(address user, uint256 profileId)
|
||||
external
|
||||
view
|
||||
returns (string memory)
|
||||
{
|
||||
return _metadataByProfileByOwner[user][profileId];
|
||||
}
|
||||
|
||||
function _setProfileMetadataURI(
|
||||
address user,
|
||||
uint256 profileId,
|
||||
string calldata metadata
|
||||
) internal {
|
||||
_metadataByProfileByOwner[user][profileId] = metadata;
|
||||
emit Events.ProfileMetadataSet(user, profileId, metadata, block.timestamp);
|
||||
}
|
||||
|
||||
function _toggleFollow(
|
||||
address follower,
|
||||
uint256[] calldata profileIds,
|
||||
|
||||
@@ -198,9 +198,8 @@ before(async function () {
|
||||
lensHub = LensHub__factory.connect(proxy.address, user);
|
||||
|
||||
// LensPeriphery
|
||||
lensPeriphery = await new LensPeriphery__factory(deployer).deploy(
|
||||
lensHub.address
|
||||
);
|
||||
lensPeriphery = await new LensPeriphery__factory(deployer).deploy(lensHub.address);
|
||||
lensPeriphery = lensPeriphery.connect(user);
|
||||
|
||||
// Currency
|
||||
currency = await new Currency__factory(deployer).deploy();
|
||||
|
||||
@@ -33,6 +33,7 @@ export const ERRORS = {
|
||||
FOLLOW_NOT_APPROVED: 'FollowNotApproved()',
|
||||
ARRAY_MISMATCH: 'ArrayMismatch()',
|
||||
CANNOT_COMMENT_ON_SELF: 'CannotCommentOnSelf',
|
||||
NOT_DISPATCHER: 'NotDispatcher()',
|
||||
ERC721_NOT_OWN: 'ERC721: transfer of token that is not own',
|
||||
ERC721_TRANSFER_NOT_OWNER_OR_APPROVED: 'ERC721: transfer caller is not owner nor approved',
|
||||
ERC721_QUERY_FOR_NONEXISTENT_TOKEN: 'ERC721: owner query for nonexistent token',
|
||||
|
||||
@@ -445,6 +445,16 @@ export async function getToggleFollowWithSigParts(
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getSetProfileMetadataURIWithSigParts(
|
||||
profileId: string | number,
|
||||
metadata: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
): Promise<{ v: number; r: string; s: string }> {
|
||||
const msgParams = buildSetProfileMetadataURIWithSigParams(profileId, metadata, nonce, deadline);
|
||||
return await getSig(msgParams);
|
||||
}
|
||||
|
||||
export async function getCollectWithSigParts(
|
||||
profileId: BigNumberish,
|
||||
pubId: string,
|
||||
@@ -863,6 +873,34 @@ const buildToggleFollowWithSigParams = (
|
||||
},
|
||||
});
|
||||
|
||||
const buildSetProfileMetadataURIWithSigParams = (
|
||||
profileId: string | number,
|
||||
metadata: string,
|
||||
nonce: number,
|
||||
deadline: string
|
||||
) => ({
|
||||
types: {
|
||||
SetProfileMetadataURIWithSig: [
|
||||
{ name: 'profileId', type: 'uint256' },
|
||||
{ name: 'metadata', type: 'string' },
|
||||
{ name: 'nonce', type: 'uint256' },
|
||||
{ name: 'deadline', type: 'uint256' },
|
||||
],
|
||||
},
|
||||
domain: {
|
||||
name: LENS_PERIPHERY_NAME,
|
||||
version: '1',
|
||||
chainId: getChainId(),
|
||||
verifyingContract: lensPeriphery.address,
|
||||
},
|
||||
value: {
|
||||
profileId: profileId,
|
||||
metadata: metadata,
|
||||
nonce: nonce,
|
||||
deadline: deadline,
|
||||
},
|
||||
});
|
||||
|
||||
const buildCollectWithSigParams = (
|
||||
profileId: BigNumberish,
|
||||
pubId: string,
|
||||
|
||||
@@ -1,311 +0,0 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { FollowNFT__factory } from '../../../typechain-types';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../../helpers/constants';
|
||||
import { ERRORS } from '../../helpers/errors';
|
||||
import {
|
||||
getAbbreviation,
|
||||
getFollowWithSigParts,
|
||||
getTimestamp,
|
||||
getToggleFollowWithSigParts,
|
||||
matchEvent,
|
||||
waitForTx,
|
||||
} from '../../helpers/utils';
|
||||
import {
|
||||
lensHub,
|
||||
lensPeriphery,
|
||||
FIRST_PROFILE_ID,
|
||||
makeSuiteCleanRoom,
|
||||
MOCK_PROFILE_HANDLE,
|
||||
testWallet,
|
||||
user,
|
||||
userTwo,
|
||||
userThree,
|
||||
userTwoAddress,
|
||||
userThreeAddress,
|
||||
MOCK_PROFILE_URI,
|
||||
userAddress,
|
||||
MOCK_FOLLOW_NFT_URI,
|
||||
OTHER_MOCK_URI,
|
||||
} from '../../__setup.spec';
|
||||
|
||||
makeSuiteCleanRoom('ToggleFollowing', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userThree).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
});
|
||||
context('Generic', function () {
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to toggle follow with an incorrect profileId', async function () {
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID + 1], [true])
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to toggle follow with array mismatch', async function () {
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID, FIRST_PROFILE_ID], [])
|
||||
).to.be.revertedWith(ERRORS.ARRAY_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to toggle follow from a profile that has been burned', async function () {
|
||||
await expect(lensHub.burn(FIRST_PROFILE_ID)).to.not.be.reverted;
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true])
|
||||
).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to toggle follow for a followNFT that is not owned by them', async function () {
|
||||
const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
const followNFT = FollowNFT__factory.connect(followNFTAddress, user);
|
||||
|
||||
await expect(
|
||||
followNFT.connect(userTwo).transferFrom(userTwoAddress, userAddress, 1)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true])
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('UserTwo should toggle follow with true value, correct event should be emitted', async function () {
|
||||
const tx = lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true]);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
userTwoAddress,
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should create another profile, userTwo follows, then toggles both, one true, one false, correct event should be emitted', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: 'otherhandle',
|
||||
imageURI: OTHER_MOCK_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID + 1], [[]])).to.not.be.reverted;
|
||||
|
||||
const tx = lensPeriphery
|
||||
.connect(userTwo)
|
||||
.toggleFollow([FIRST_PROFILE_ID, FIRST_PROFILE_ID + 1], [true, false]);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
userTwoAddress,
|
||||
[FIRST_PROFILE_ID, FIRST_PROFILE_ID + 1],
|
||||
[true, false],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('UserTwo should toggle follow with false value, correct event should be emitted', async function () {
|
||||
const tx = lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [false]);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
userTwoAddress,
|
||||
[FIRST_PROFILE_ID],
|
||||
[false],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to toggle follow with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to toggle follow with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to toggle follow with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to toggle follow a nonexistent profile with sig', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
const INVALID_PROFILE = FIRST_PROFILE_ID + 1;
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[INVALID_PROFILE],
|
||||
[true],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [INVALID_PROFILE],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should toggle follow profile 1 to true with sig, correct event should be emitted ', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
const tx = lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
testWallet.address,
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('TestWallet should toggle follow profile 1 to false with sig, correct event should be emitted ', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const enabled = false;
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[enabled],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
const tx = lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [enabled],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
testWallet.address,
|
||||
[FIRST_PROFILE_ID],
|
||||
[enabled],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,18 @@
|
||||
import '@nomiclabs/hardhat-ethers';
|
||||
import { expect } from 'chai';
|
||||
import { keccak256, toUtf8Bytes } from 'ethers/lib/utils';
|
||||
import { UIDataProvider__factory } from '../../typechain-types';
|
||||
import { ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { FollowNFT__factory, UIDataProvider__factory } from '../../typechain-types';
|
||||
import { MAX_UINT256, ZERO_ADDRESS } from '../helpers/constants';
|
||||
import { ERRORS } from '../helpers/errors';
|
||||
import {
|
||||
getDecodedSvgImage,
|
||||
getMetadataFromBase64TokenUri,
|
||||
getSetProfileMetadataURIWithSigParts,
|
||||
getTimestamp,
|
||||
getToggleFollowWithSigParts,
|
||||
loadTestResourceAsUtf8String,
|
||||
matchEvent,
|
||||
waitForTx,
|
||||
} from '../helpers/utils';
|
||||
import {
|
||||
approvalFollowModule,
|
||||
@@ -35,6 +40,9 @@ import {
|
||||
userTwo,
|
||||
userTwoAddress,
|
||||
abiCoder,
|
||||
userThree,
|
||||
testWallet,
|
||||
lensPeriphery,
|
||||
} from '../__setup.spec';
|
||||
|
||||
/**
|
||||
@@ -746,4 +754,518 @@ makeSuiteCleanRoom('Misc', function () {
|
||||
expect(pubByHandleStruct.collectNFT).to.eq(ZERO_ADDRESS);
|
||||
});
|
||||
});
|
||||
|
||||
context('LensPeriphery', async function () {
|
||||
context('ToggleFollowing', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(lensHub.connect(userTwo).follow([FIRST_PROFILE_ID], [[]])).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userThree).follow([FIRST_PROFILE_ID], [[]])
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(testWallet).follow([FIRST_PROFILE_ID], [[]])
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Generic', function () {
|
||||
context('Negatives', function () {
|
||||
it('UserTwo should fail to toggle follow with an incorrect profileId', async function () {
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID + 1], [true])
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to toggle follow with array mismatch', async function () {
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID, FIRST_PROFILE_ID], [])
|
||||
).to.be.revertedWith(ERRORS.ARRAY_MISMATCH);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to toggle follow from a profile that has been burned', async function () {
|
||||
await expect(lensHub.burn(FIRST_PROFILE_ID)).to.not.be.reverted;
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true])
|
||||
).to.be.revertedWith(ERRORS.TOKEN_DOES_NOT_EXIST);
|
||||
});
|
||||
|
||||
it('UserTwo should fail to toggle follow for a followNFT that is not owned by them', async function () {
|
||||
const followNFTAddress = await lensHub.getFollowNFT(FIRST_PROFILE_ID);
|
||||
const followNFT = FollowNFT__factory.connect(followNFTAddress, user);
|
||||
|
||||
await expect(
|
||||
followNFT.connect(userTwo).transferFrom(userTwoAddress, userAddress, 1)
|
||||
).to.not.be.reverted;
|
||||
|
||||
await expect(
|
||||
lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true])
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('UserTwo should toggle follow with true value, correct event should be emitted', async function () {
|
||||
const tx = lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [true]);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
userTwoAddress,
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('User should create another profile, userTwo follows, then toggles both, one true, one false, correct event should be emitted', async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: 'otherhandle',
|
||||
imageURI: OTHER_MOCK_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensHub.connect(userTwo).follow([FIRST_PROFILE_ID + 1], [[]])
|
||||
).to.not.be.reverted;
|
||||
|
||||
const tx = lensPeriphery
|
||||
.connect(userTwo)
|
||||
.toggleFollow([FIRST_PROFILE_ID, FIRST_PROFILE_ID + 1], [true, false]);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
userTwoAddress,
|
||||
[FIRST_PROFILE_ID, FIRST_PROFILE_ID + 1],
|
||||
[true, false],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('UserTwo should toggle follow with false value, correct event should be emitted', async function () {
|
||||
const tx = lensPeriphery.connect(userTwo).toggleFollow([FIRST_PROFILE_ID], [false]);
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
userTwoAddress,
|
||||
[FIRST_PROFILE_ID],
|
||||
[false],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', function () {
|
||||
context('Negatives', function () {
|
||||
it('TestWallet should fail to toggle follow with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to toggle follow with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to toggle follow with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to toggle follow a nonexistent profile with sig', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
const INVALID_PROFILE = FIRST_PROFILE_ID + 1;
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[INVALID_PROFILE],
|
||||
[true],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [INVALID_PROFILE],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.FOLLOW_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should toggle follow profile 1 to true with sig, correct event should be emitted ', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
const tx = lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [true],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
testWallet.address,
|
||||
[FIRST_PROFILE_ID],
|
||||
[true],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('TestWallet should toggle follow profile 1 to false with sig, correct event should be emitted ', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const enabled = false;
|
||||
const { v, r, s } = await getToggleFollowWithSigParts(
|
||||
[FIRST_PROFILE_ID],
|
||||
[enabled],
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
|
||||
const tx = lensPeriphery.toggleFollowWithSig({
|
||||
follower: testWallet.address,
|
||||
profileIds: [FIRST_PROFILE_ID],
|
||||
enables: [enabled],
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
});
|
||||
|
||||
const receipt = await waitForTx(tx);
|
||||
|
||||
expect(receipt.logs.length).to.eq(1);
|
||||
matchEvent(receipt, 'FollowsToggled', [
|
||||
testWallet.address,
|
||||
[FIRST_PROFILE_ID],
|
||||
[enabled],
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Profile Metadata URI', function () {
|
||||
const MOCK_DATA = 'd171c8b1d364bb34553299ab686caa41ac7a2209d4a63e25947764080c4681da';
|
||||
|
||||
context('Generic', function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.createProfile({
|
||||
to: userAddress,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', function () {
|
||||
it("User should fail to set profile metadata URI as dispatcher without being the profile's dispatcher", async function () {
|
||||
await expect(
|
||||
lensPeriphery.dispatcherSetProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA)
|
||||
).to.be.revertedWith(ERRORS.NOT_DISPATCHER);
|
||||
});
|
||||
|
||||
it('Fetching profile metadata for a profile that does not exist yet should fail', async function () {
|
||||
await expect(
|
||||
lensPeriphery.getProfileMetadataURI(FIRST_PROFILE_ID + 1)
|
||||
).to.be.revertedWith(ERRORS.ERC721_QUERY_FOR_NONEXISTENT_TOKEN);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('User should set profile metadata for a profile that does not exist, fetched data should be accurate and revert without passing the owner', async function () {
|
||||
const secondProfileId = FIRST_PROFILE_ID + 1;
|
||||
await expect(
|
||||
lensPeriphery.setProfileMetadataURI(secondProfileId, MOCK_DATA)
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(
|
||||
await lensPeriphery.getProfileMetadataURIByOwner(userAddress, secondProfileId)
|
||||
).to.eq(MOCK_DATA);
|
||||
await expect(lensPeriphery.getProfileMetadataURI(secondProfileId)).to.be.revertedWith(
|
||||
ERRORS.ERC721_QUERY_FOR_NONEXISTENT_TOKEN
|
||||
);
|
||||
});
|
||||
|
||||
it("User should set user two as dispatcher, user two should set profile metadata URI for user one's profile, fetched data should be accurate", async function () {
|
||||
await expect(
|
||||
lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress)
|
||||
).to.not.be.reverted;
|
||||
await expect(
|
||||
lensPeriphery
|
||||
.connect(userTwo)
|
||||
.dispatcherSetProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA)
|
||||
).to.not.be.reverted;
|
||||
|
||||
expect(
|
||||
await lensPeriphery.getProfileMetadataURIByOwner(userAddress, FIRST_PROFILE_ID)
|
||||
).to.eq(MOCK_DATA);
|
||||
expect(await lensPeriphery.getProfileMetadataURI(FIRST_PROFILE_ID)).to.eq(MOCK_DATA);
|
||||
});
|
||||
|
||||
it('Setting profile metadata should emit the correct event', async function () {
|
||||
const tx = await waitForTx(
|
||||
lensPeriphery.setProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA)
|
||||
);
|
||||
|
||||
matchEvent(tx, 'ProfileMetadataSet', [
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_DATA,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
|
||||
it('Setting profile metadata via dispatcher should emit the correct event', async function () {
|
||||
await expect(
|
||||
lensHub.setDispatcher(FIRST_PROFILE_ID, userTwoAddress)
|
||||
).to.not.be.reverted;
|
||||
|
||||
const tx = await waitForTx(
|
||||
lensPeriphery
|
||||
.connect(userTwo)
|
||||
.dispatcherSetProfileMetadataURI(FIRST_PROFILE_ID, MOCK_DATA)
|
||||
);
|
||||
|
||||
matchEvent(tx, 'ProfileMetadataSet', [
|
||||
userAddress,
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_DATA,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
context('Meta-tx', async function () {
|
||||
beforeEach(async function () {
|
||||
await expect(
|
||||
lensHub.connect(testWallet).createProfile({
|
||||
to: testWallet.address,
|
||||
handle: MOCK_PROFILE_HANDLE,
|
||||
imageURI: MOCK_PROFILE_URI,
|
||||
followModule: ZERO_ADDRESS,
|
||||
followModuleData: [],
|
||||
followNFTURI: MOCK_FOLLOW_NFT_URI,
|
||||
})
|
||||
).to.not.be.reverted;
|
||||
});
|
||||
|
||||
context('Negatives', async function () {
|
||||
it('TestWallet should fail to set profile metadata URI with sig with signature deadline mismatch', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getSetProfileMetadataURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_DATA,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.setProfileMetadataURIWithSig({
|
||||
user: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
metadata: MOCK_DATA,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set profile metadata URI with sig with invalid deadline', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getSetProfileMetadataURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_DATA,
|
||||
nonce,
|
||||
'0'
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.setProfileMetadataURIWithSig({
|
||||
user: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
metadata: MOCK_DATA,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: '0',
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_EXPIRED);
|
||||
});
|
||||
|
||||
it('TestWallet should fail to set profile metadata URI with sig with invalid nonce', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getSetProfileMetadataURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_DATA,
|
||||
nonce + 1,
|
||||
MAX_UINT256
|
||||
);
|
||||
await expect(
|
||||
lensPeriphery.setProfileMetadataURIWithSig({
|
||||
user: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
metadata: MOCK_DATA,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
).to.be.revertedWith(ERRORS.SIGNATURE_INVALID);
|
||||
});
|
||||
});
|
||||
|
||||
context('Scenarios', function () {
|
||||
it('TestWallet should set profile metadata URI with sig, fetched data should be accurate and correct event should be emitted', async function () {
|
||||
const nonce = (await lensPeriphery.sigNonces(testWallet.address)).toNumber();
|
||||
|
||||
const { v, r, s } = await getSetProfileMetadataURIWithSigParts(
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_DATA,
|
||||
nonce,
|
||||
MAX_UINT256
|
||||
);
|
||||
const tx = await waitForTx(
|
||||
lensPeriphery.setProfileMetadataURIWithSig({
|
||||
user: testWallet.address,
|
||||
profileId: FIRST_PROFILE_ID,
|
||||
metadata: MOCK_DATA,
|
||||
sig: {
|
||||
v,
|
||||
r,
|
||||
s,
|
||||
deadline: MAX_UINT256,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
expect(await lensPeriphery.getProfileMetadataURI(FIRST_PROFILE_ID)).to.eq(MOCK_DATA);
|
||||
expect(
|
||||
await lensPeriphery.getProfileMetadataURIByOwner(testWallet.address, FIRST_PROFILE_ID)
|
||||
).to.eq(MOCK_DATA);
|
||||
|
||||
matchEvent(tx, 'ProfileMetadataSet', [
|
||||
testWallet.address,
|
||||
FIRST_PROFILE_ID,
|
||||
MOCK_DATA,
|
||||
await getTimestamp(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user