Merge pull request #86 from aave/feat/peripheral-profile-metadata

This commit is contained in:
Zer0dot
2022-04-04 11:03:35 -04:00
committed by GitHub
9 changed files with 691 additions and 320 deletions

View File

@@ -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.
*

View File

@@ -20,6 +20,7 @@ library Errors {
error ProfileCreatorNotWhitelisted();
error NotProfileOwner();
error NotProfileOwnerOrDispatcher();
error NotDispatcher();
error PublicationDoesNotExist();
error HandleTaken();
error HandleLengthInvalid();

View File

@@ -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
);
}

View File

@@ -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,

View File

@@ -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();

View File

@@ -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',

View File

@@ -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,

View File

@@ -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(),
]);
});
});
});
});

View File

@@ -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(),
]);
});
});
});
});
});
});