Merge pull request #258 from fleekxyz/release/release-v0.0.8

Release: release v0.0.8 to main
This commit is contained in:
Camila Sosa Morales 2023-05-15 13:44:04 -03:00 committed by GitHub
commit c32d4d5771
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 6719 additions and 1307 deletions

View File

@ -1,6 +1,6 @@
# Fleek Non Fungible Apps
# Fleek Non-Fungible Apps
**The repository for Fleek Non Fungible Apps project**
**The repository for Fleek Non-Fungible Apps project**
> 🚧 IMPORTANT 🚧 - This initiative is under development, so this repo should be treated as a WIP. The goals and the roadmap might change as the project is shaped.
@ -21,44 +21,48 @@ You can find the wiki [here](https://github.com/fleekxyz/non-fungible-apps/wiki)
### 📁 Project Structure
Inside the root folder you are going to find:
- [contracts](./contracts): all the developed contracts
- [contracts](./contracts): All the developed contracts
- [subgraph](./subgraph): The Graph project related code
- [ui](./ui): a web application to interact with deployed contracts
- [serverless](./serverless): The serverless and Mongo/Prisma set-up
- [ui](./ui): A web application to interact with deployed contracts
You can see breakdowns of other folders in the README within those folders.
### Contracts
Within the project is the contracts folder which houses the contracts, utils, tests and deployment scripts associated with the Solidity smart contracts. Check the [contracts readme](./contracts/README.md) for more info.
### 🖥️ User Interface
Within the project is included a [React](https://reactjs.org/) web application to expose and test the interaction with deployed scripts. Check the [UI readme](./ui/README.md) for more info.
Within the project is the contracts folder which houses the contracts, utils, tests, and deployment scripts associated with the Solidity smart contracts. Check the [contracts readme](./contracts/README.md) for more info.
### Subgraph
In order to index data offchain, we use TheGraph and this section is the code required for our subgraph. Check the [subgraph readme](./subgraph/README.md) for more info.
To index data off-chain, we use TheGraph and this section is the code required for our subgraph. Check the [subgraph readme](./subgraph/README.md) for more info.
### Serverless
For verification purposes and our off-chain stack, we are using a MongoDB instance integrated with Prisma and serverless handlers. Check the [serverless readme](./serverless/README.md) for more info.
### User Interface (UI)
Within the project is included a [React](https://reactjs.org/) web application to expose and test the interaction with deployed scripts. Check the [UI readme](./ui/README.md) for more info.
### 💅 Code Styling
For code formatting we are using [Prettier](https://prettier.io/) and following the [styling guide from Solidity documentation](https://docs.soliditylang.org/en/v0.8.16/style-guide.html). For formatting the code you are able to run:
For code formatting, we are using [Prettier](https://prettier.io/) and following the [styling guide from Solidity documentation](https://docs.soliditylang.org/en/v0.8.16/style-guide.html). For formatting the code you can run:
```
$ yarn format
```
> ⚠️ Please make sure you are following the code styling guid before pushing code
> ⚠️ Please make sure you are following the code styling guide before pushing the code
## 🛣️ Roadmap
Our goal is to reach a point where trustable Solidity contracts can be used for identifying properly the data about web3 applications. Within that goal, we want to also provide ways for users to organize and list information about their application. To get at this we are currently starting with:
Our goal is to reach a point where trustable Solidity contracts can be used for identifying properly the data about web3 applications. Within that goal, we want to also provide ways for users to organize and list information about their applications. To get at this we are currently starting with:
- Define trustable and extendable smart contracts and standards
- Prove how the concept would be applicable using static sites
- Prove community hosted apps via these contracts
- Prove how the concept would be applied using static sites
- Prove community-hosted apps via these contracts
Later on, when the initiative prove its value, a service will be added to Fleek's platform in a friendly way for anyone be able to get their applications onboard.
Later on, when the initiative proves its value, a service will be added to Fleek's platform in a friendly way for anyone to be able to get their applications onboard.
## 💡 Proof of concept
@ -66,7 +70,7 @@ The proof of concept was concluded last year and you can reach more information
## 📚 Dependency Highlights
We use the following libraries to develop Fleek Non Fungible Apps
We use the following libraries to develop Fleek Non-Fungible Apps
- [Eslint](https://eslint.org/) + [Prettier](https://prettier.io/)
- [Ethers](https://docs.ethers.io/v5/)
@ -79,11 +83,11 @@ We use the following libraries to develop Fleek Non Fungible Apps
## 🙏 Contributing
This is an open source initiative! Any new idea is welcome, if you want to help us to improve the project please checkout [the contributing guide](/CONTRIBUTING.md).
This is an open-source initiative! Any new idea is welcome, if you want to help us to improve the project please check out [the contributing guide](/CONTRIBUTING.md).
## 📜 License
Fleek Non Fungible Apps is released under the [MIT License](LICENSE).
Fleek Non-Fungible Apps is released under the [MIT License](LICENSE).
## 🐛 Bug reporting

View File

@ -39,6 +39,7 @@ contract FleekERC721 is
string ENS,
string commitHash,
string gitRepository,
string ipfsHash,
string logo,
uint24 color,
bool accessPointAutoApproval,
@ -108,6 +109,7 @@ contract FleekERC721 is
string calldata ens,
string memory commitHash,
string memory gitRepository,
string memory ipfsHash,
string memory logo,
uint24 color,
bool accessPointAutoApproval,
@ -131,7 +133,7 @@ contract FleekERC721 is
// The mint interaction is considered to be the first build of the site. Updates from now on all increment the currentBuild by one and update the mapping.
app.currentBuild = 0;
app.builds[0] = Build(commitHash, gitRepository);
app.builds[0] = Build(commitHash, gitRepository, ipfsHash, externalURL);
emit NewMint(
tokenId,
@ -141,6 +143,7 @@ contract FleekERC721 is
ens,
commitHash,
gitRepository,
ipfsHash,
logo,
color,
accessPointAutoApproval,
@ -396,11 +399,14 @@ contract FleekERC721 is
function setTokenBuild(
uint256 tokenId,
string memory _commitHash,
string memory _gitRepository
string memory _gitRepository,
string memory _ipfsHash,
string memory _domain
) public virtual requireTokenRole(tokenId, TokenRoles.Controller) {
_requireMinted(tokenId);
_apps[tokenId].builds[++_apps[tokenId].currentBuild] = Build(_commitHash, _gitRepository);
emit MetadataUpdate(tokenId, "build", [_commitHash, _gitRepository], msg.sender);
_apps[tokenId].builds[++_apps[tokenId].currentBuild] = Build(_commitHash, _gitRepository, _ipfsHash, _domain);
// Note from Nima: should we update the externalURL field with each new domain?
emit MetadataUpdate(tokenId, "build", [_commitHash, _gitRepository, _ipfsHash, _domain], msg.sender);
}
/**

View File

@ -24,7 +24,7 @@ interface IERCX {
*/
event MetadataUpdate(uint256 indexed _tokenId, string key, string value, address indexed triggeredBy);
event MetadataUpdate(uint256 indexed _tokenId, string key, uint24 value, address indexed triggeredBy);
event MetadataUpdate(uint256 indexed _tokenId, string key, string[2] value, address indexed triggeredBy);
event MetadataUpdate(uint256 indexed _tokenId, string key, string[4] value, address indexed triggeredBy);
event MetadataUpdate(uint256 indexed _tokenId, string key, bool value, address indexed triggeredBy);
/**
@ -33,6 +33,8 @@ interface IERCX {
struct Build {
string commitHash;
string gitRepository;
string ipfsHash;
string domain;
}
/**
@ -84,7 +86,13 @@ interface IERCX {
/**
* @dev Sets a minted token's build.
*/
function setTokenBuild(uint256 tokenId, string memory commitHash, string memory gitRepository) external;
function setTokenBuild(
uint256 tokenId,
string memory commitHash,
string memory gitRepository,
string memory ipfsHash,
string memory domain
) external;
/**
* @dev Returns the token metadata for a given tokenId.

View File

@ -25,7 +25,7 @@ const libraryDeployment = async (hre) => {
const libContract = await hre.ethers.getContractFactory(lib);
const libInstance = await libContract.deploy();
await libInstance.deployed();
deployStore(hre.network.name, lib, libInstance);
await deployStore(hre.network.name, lib, libInstance, false);
console.log(`Library "${lib}" deployed at ${libInstance.address}`);
libraries[lib] = libInstance.address;
}
@ -70,7 +70,6 @@ module.exports = async (taskArgs, hre) => {
`Contract ${CONTRACT_NAME} upgraded at "${deployResult.address}" by account "${deployResult.signer.address}"`
);
console.log('\x1b[0m');
await deployStore(network, CONTRACT_NAME, deployResult);
} catch (e) {
if (
e.message === 'new-proxy-instance' ||

View File

@ -33,22 +33,16 @@ const getBuildData = async (contractName) => {
};
};
const deployStore = async (network, contractName, contract) => {
const deployStore = async (network, contractName, contract, isProxy = true) => {
const filePath = getDeployFilePath(network, contractName);
const { buildId, solcInput, abi, bytecode, metadata, storageLayout } =
await getBuildData(contractName);
const implementationAddress = await getImplementationAddress(
hre.network.provider,
contract.address
);
const data = {
buildId,
timestamp: new Date().toLocaleString('en-US'),
address: contract.address,
implementationAddress,
transactionHash: contract.deployTransaction.hash,
args: contract.deployTransaction.args,
gasPrice: contract.deployTransaction.gasPrice.toNumber(),
@ -58,6 +52,14 @@ const deployStore = async (network, contractName, contract) => {
storageLayout,
};
if (isProxy) {
const implementationAddress = await getImplementationAddress(
hre.network.provider,
contract.address
);
data.implementationAddress = implementationAddress;
}
try {
const solcInputsFilePath =
filePath.split('/').slice(0, -1).join('/') +

View File

@ -369,29 +369,31 @@ contract Test_FleekERC721_AccessControl is Test_FleekERC721_Base, Test_FleekERC7
function test_setTokenBuild() public {
string memory commitHash = "commitHash";
string memory gitRepository = "gitRepository";
string memory ipfsHash = "ipfsHash";
string memory domain = "domain";
// ColletionOwner
vm.prank(collectionOwner);
expectRevertWithTokenRole(tokenId, FleekAccessControl.TokenRoles.Controller);
CuT.setTokenBuild(tokenId, commitHash, gitRepository);
CuT.setTokenBuild(tokenId, commitHash, gitRepository, ipfsHash, domain);
// CollectionVerifier
vm.prank(collectionVerifier);
expectRevertWithTokenRole(tokenId, FleekAccessControl.TokenRoles.Controller);
CuT.setTokenBuild(tokenId, commitHash, gitRepository);
CuT.setTokenBuild(tokenId, commitHash, gitRepository, ipfsHash, domain);
// TokenOwner
vm.prank(tokenOwner);
CuT.setTokenBuild(tokenId, commitHash, gitRepository);
CuT.setTokenBuild(tokenId, commitHash, gitRepository, ipfsHash, domain);
// TokenController
vm.prank(tokenController);
CuT.setTokenBuild(tokenId, commitHash, gitRepository);
CuT.setTokenBuild(tokenId, commitHash, gitRepository, ipfsHash, domain);
// AnyAddress
vm.prank(anyAddress);
expectRevertWithTokenRole(tokenId, FleekAccessControl.TokenRoles.Controller);
CuT.setTokenBuild(tokenId, commitHash, gitRepository);
CuT.setTokenBuild(tokenId, commitHash, gitRepository, ipfsHash, domain);
}
function test_burn() public {

View File

@ -54,6 +54,7 @@ contract Test_FleekERC721_Billing is Test_FleekERC721_Base, Test_FleekERC721_Bil
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.APP_IPFS_HASH,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS,
@ -75,6 +76,7 @@ contract Test_FleekERC721_Billing is Test_FleekERC721_Base, Test_FleekERC721_Bil
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.APP_IPFS_HASH,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS,
@ -98,6 +100,7 @@ contract Test_FleekERC721_Billing is Test_FleekERC721_Base, Test_FleekERC721_Bil
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.APP_IPFS_HASH,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
TestConstants.APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS,

View File

@ -15,6 +15,8 @@ library TestConstants {
string public constant APP_GIT_REPOSITORY = "https://github.com/fleekxyz/non-fungible-apps";
string public constant APP_IPFS_HASH = "mtwirsqawjuoloq2gvtyug2tc3jbf5htm2zeo4rsknfiv3fdp46a";
uint24 public constant APP_COLOR = 0x123456;
bool public constant APP_ACCESS_POINT_AUTO_APPROVAL_SETTINGS = true;

View File

@ -39,6 +39,8 @@ contract Test_FleekERC721_GetToken is Test_FleekERC721_Base {
string memory newENS,
string memory newCommitHash,
string memory newRepository,
string memory newIpfsHash,
string memory newDomain,
string memory newLogo,
uint24 newColor
) public {
@ -47,7 +49,7 @@ contract Test_FleekERC721_GetToken is Test_FleekERC721_Base {
CuT.setTokenExternalURL(tokenId, newExternalURL);
transferENS(newENS, deployer);
CuT.setTokenENS(tokenId, newENS);
CuT.setTokenBuild(tokenId, newCommitHash, newRepository);
CuT.setTokenBuild(tokenId, newCommitHash, newRepository, newIpfsHash, newDomain);
CuT.setTokenLogoAndColor(tokenId, newLogo, newColor);
(

View File

@ -36,6 +36,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
"fleek.eth",
"94e8ba38568aea4fb277a37a4c472d94a6ce880a",
"https://github.com/a-different/repository",
"mtwirsqawjuoloq2gvtyug2tc3jbf5htm2zeo4rsknfiv3fdp46a",
TestConstants.LOGO_1,
0x654321,
false,
@ -56,6 +57,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
"fleek.eth",
"94e8ba38568aea4fb277a37a4c472d94a6ce880a",
"https://github.com/a-different/repository",
"mtwirsqawjuoloq2gvtyug2tc3jbf5htm2zeo4rsknfiv3fdp46a",
TestConstants.LOGO_1,
0x654321,
true,
@ -81,6 +83,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
string memory ens,
string memory commitHash,
string memory gitRepository,
string memory ipfsHash,
string memory logo,
uint24 color,
bool autoApprovalAp
@ -95,6 +98,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
ens,
commitHash,
gitRepository,
ipfsHash,
logo,
color,
autoApprovalAp,
@ -115,6 +119,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.APP_IPFS_HASH,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
false,
@ -131,6 +136,7 @@ contract Test_FleekERC721_Mint is Test_FleekERC721_Base {
"",
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.APP_IPFS_HASH,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
false,

View File

@ -81,6 +81,7 @@ abstract contract Test_FleekERC721_Base is Test, Test_FleekERC721_Assertions {
TestConstants.APP_ENS,
TestConstants.APP_COMMIT_HASH,
TestConstants.APP_GIT_REPOSITORY,
TestConstants.APP_IPFS_HASH,
TestConstants.LOGO_0,
TestConstants.APP_COLOR,
false, // Auto Approval Is OFF

View File

@ -6,7 +6,7 @@ import "./TestBase.sol";
contract Test_FleekERC721_TokenURIAssertions is Test {
event MetadataUpdate(uint256 indexed _tokenId, string key, string value, address indexed triggeredBy);
event MetadataUpdate(uint256 indexed _tokenId, string key, string[2] value, address indexed triggeredBy);
event MetadataUpdate(uint256 indexed _tokenId, string key, string[4] value, address indexed triggeredBy);
event MetadataUpdate(uint256 indexed _tokenId, string key, uint24 value, address indexed triggeredBy);
function expectMetadataUpdate(
@ -22,7 +22,7 @@ contract Test_FleekERC721_TokenURIAssertions is Test {
function expectMetadataUpdate(
uint256 _tokenId,
string memory key,
string[2] memory value,
string[4] memory value,
address triggeredBy
) public {
vm.expectEmit(true, true, true, true);
@ -57,7 +57,13 @@ contract Test_FleekERC721_TokenURI is Test_FleekERC721_Base, Test_FleekERC721_To
CuT.setTokenExternalURL(tokenId, "https://new-url.com");
transferENS("new-ens.eth", deployer);
CuT.setTokenENS(tokenId, "new-ens.eth");
CuT.setTokenBuild(tokenId, "ce1a3fc141e29f8e1d00a654e156c4982d7711bf", "https://github.com/other/repo");
CuT.setTokenBuild(
tokenId,
"ce1a3fc141e29f8e1d00a654e156c4982d7711bf",
"https://github.com/other/repo",
"ipfsHash",
"domain"
);
CuT.setTokenLogoAndColor(tokenId, TestConstants.LOGO_1, 0x654321);
CuT.setTokenVerified(tokenId, true);
@ -96,10 +102,16 @@ contract Test_FleekERC721_TokenURI is Test_FleekERC721_Base, Test_FleekERC721_To
expectMetadataUpdate(
tokenId,
"build",
["ce1a3fc141e29f8e1d00a654e156c4982d7711bf", "https://github.com/other/repo"],
["ce1a3fc141e29f8e1d00a654e156c4982d7711bf", "https://github.com/other/repo", "ipfshash", "domain"],
deployer
);
CuT.setTokenBuild(tokenId, "ce1a3fc141e29f8e1d00a654e156c4982d7711bf", "https://github.com/other/repo");
CuT.setTokenBuild(
tokenId,
"ce1a3fc141e29f8e1d00a654e156c4982d7711bf",
"https://github.com/other/repo",
"ipfshash",
"domain"
);
expectMetadataUpdate(tokenId, "logo", TestConstants.LOGO_1, deployer);
CuT.setTokenLogo(tokenId, TestConstants.LOGO_1);

View File

@ -21,6 +21,7 @@ describe('FleekERC721.Billing', () => {
MintParams.ens,
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,

View File

@ -187,6 +187,7 @@ describe('FleekERC721.CollectionRoles', () => {
TestConstants.MintParams.ens,
TestConstants.MintParams.commitHash,
TestConstants.MintParams.gitRepository,
TestConstants.MintParams.ipfsHash,
TestConstants.MintParams.logo,
TestConstants.MintParams.color,
TestConstants.MintParams.accessPointAutoApprovalSettings,

View File

@ -22,6 +22,7 @@ describe('FleekERC721.ENS', () => {
'app.eth',
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
@ -43,6 +44,7 @@ describe('FleekERC721.ENS', () => {
'app.eth',
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,

View File

@ -14,6 +14,7 @@ describe('FleekERC721.GetLastTokenId', () => {
TestConstants.MintParams.ens,
TestConstants.MintParams.commitHash,
TestConstants.MintParams.gitRepository,
TestConstants.MintParams.ipfsHash,
TestConstants.MintParams.logo,
TestConstants.MintParams.color,
false,

View File

@ -23,6 +23,7 @@ export const TestConstants = Object.freeze({
externalUrl: 'https://fleek.co',
commitHash: 'b72e47171746b6a9e29b801af9cb655ecf4d665c',
gitRepository: 'https://github.com/fleekxyz/non-fungible-apps',
ipfsHash: 'mtwirsqawjuoloq2gvtyug2tc3jbf5htm2zeo4rsknfiv3fdp46a',
logo: 'data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBoZWlnaHQ9IjI1MDAiIHdpZHRoPSIyMTgzIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjQgMTQxLjUzMTk5OTk5OTk5OTk4Ij48cGF0aCBkPSJNMTAuMzgzIDEyNi44OTRMMCAwbDEyNCAuMjU1LTEwLjk3OSAxMjYuNjM5LTUwLjU1MyAxNC42Mzh6IiBmaWxsPSIjZTM0ZjI2Ii8+PHBhdGggZD0iTTYyLjQ2OCAxMjkuMjc3VjEyLjA4NWw1MS4wNjQuMTctOS4xMDYgMTA0Ljg1MXoiIGZpbGw9IiNlZjY1MmEiLz48cGF0aCBkPSJNOTkuNDkgNDEuMzYybDEuNDQ2LTE1LjQ5SDIyLjM4M2w0LjM0IDQ3LjQ5aDU0LjIxM0w3OC44MSA5My42MTdsLTE3LjM2MiA0LjY4LTE3LjYxNy01LjEwNi0uOTM2LTEyLjA4NUgyNy4zMTlsMi4xMjggMjQuNjgxIDMyIDguOTM2IDMyLjI1NS04LjkzNiA0LjM0LTQ4LjE3SDQxLjEwN0wzOS40OSA0MS4zNjJ6IiBmaWxsPSIjZmZmIi8+PC9zdmc+',
color: 0xe34f26,
accessPointAutoApprovalSettings: false,

View File

@ -2,6 +2,6 @@ export const Events = Object.freeze({
MetadataUpdate: {
string: 'MetadataUpdate(uint256,string,string,address)',
uint24: 'MetadataUpdate(uint256,string,uint24,address)',
stringTuple: 'MetadataUpdate(uint256,string,string[2],address)',
stringArray4: 'MetadataUpdate(uint256,string,string[4],address)',
},
});

View File

@ -44,6 +44,7 @@ export abstract class Fixtures {
TestConstants.MintParams.ens,
TestConstants.MintParams.commitHash,
TestConstants.MintParams.gitRepository,
TestConstants.MintParams.ipfsHash,
TestConstants.MintParams.logo,
TestConstants.MintParams.color,
TestConstants.MintParams.accessPointAutoApprovalSettings,

View File

@ -17,6 +17,7 @@ describe('FleekERC721.Minting', () => {
MintParams.ens,
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
@ -40,6 +41,7 @@ describe('FleekERC721.Minting', () => {
MintParams.ens,
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
@ -66,6 +68,7 @@ describe('FleekERC721.Minting', () => {
MintParams.ens,
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,
@ -87,6 +90,7 @@ describe('FleekERC721.Minting', () => {
'',
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
MintParams.accessPointAutoApprovalSettings,

View File

@ -18,6 +18,7 @@ describe('FleekERC721.Pausable', () => {
MintParams.ens,
MintParams.commitHash,
MintParams.gitRepository,
MintParams.ipfsHash,
MintParams.logo,
MintParams.color,
false,

View File

@ -51,12 +51,20 @@ describe('FleekERC721.UpdateProperties', () => {
it('should emit event for build change', async () => {
const { contract, tokenId, owner } = fixture;
await expect(contract.setTokenBuild(tokenId, 'commitHash', 'gitRepository'))
.to.emit(contract, Events.MetadataUpdate.stringTuple)
await expect(
contract.setTokenBuild(
tokenId,
'commitHash',
'gitRepository',
'ipfsHash',
'domain'
)
)
.to.emit(contract, Events.MetadataUpdate.stringArray4)
.withArgs(
tokenId,
'build',
['commitHash', 'gitRepository'],
['commitHash', 'gitRepository', 'ipfsHash', 'domain'],
owner.address
);
});

View File

@ -99,6 +99,7 @@ describe('Deploy', () => {
TestConstants.MintParams.ens,
TestConstants.MintParams.commitHash,
TestConstants.MintParams.gitRepository,
TestConstants.MintParams.ipfsHash,
TestConstants.MintParams.logo,
TestConstants.MintParams.color,
TestConstants.MintParams.accessPointAutoApprovalSettings,

View File

@ -33,6 +33,7 @@
"pinst": "^3.0.0",
"prettier": "^2.8.4",
"prettier-plugin-solidity": "^1.0.0",
"serverless-offline": "^12.0.4",
"typescript": "^4.9.5"
}
}

7
serverless/.env.example Normal file
View File

@ -0,0 +1,7 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mongodb+srv://root:randompassword@cluster0.ab1cd.mongodb.net/mydb?retryWrites=true&w=majority"

View File

@ -36,3 +36,31 @@ To deploy to development environment:
To deploy to production environment:
`yarn sls deploy --stage prd`
### Running MongoDB
The first step to run MongoDB is making sure the service is installed on the machine locally. You can check the [official MongoDB website](https://www.mongodb.com/docs/manual/installation/#mongodb-installation-tutorials) for more information on the installation process.
To process database transactions such as `create` calls, Prisma needs the MongoDB instance to be running as a replica set. Run the commands below to start a replica set with `mongod` and `mongosh`:
```
// You should replace the dbpath with the actual path on your machine and assign a name to your replica set. (Default path on linux is: /var/lib/mongodb)
// Do not close the terminal tab after running mongod.
$ sudo mongod --port 27017 --dbpath /path/to/db --replSet replicaName --bind_ip localhost,127.0.0.1
// Start a mongosh session and run the replica set initiation command in the mongo shell.
$ mongosh
> rs.initiate()
```
Make sure you copy the connection string that is presented in the `Connecting to` field when the mongosh service starts to run. We need the connection string to access the replica set. Rename the `.env.example` file to `.env` and replace the connection string placeholder in the file with the one you copied.
### Prisma configuration
In order to use and integrate Prisma, both of the `prisma` and `@prisma/client` packages are needed. The `prisma` package reads the schema and generates a version of Prisma Client that is tailored to our modules.
Run the following commands to install the packages and generate the customized Prisma Client version based on the schema:
```
yarn add prisma @prisma/client
yarn prisma:generate
```

View File

@ -0,0 +1,32 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Connect the client
await prisma.$connect();
// Query the total count of tokens
const allTokens = await prisma.tokens.findMany();
console.log('Total tokens:');
console.log(allTokens.length);
// Query the token associated with tokenId 1
const tokenOne = await prisma.tokens.findRaw({
filter: {
tokenId: 1,
},
});
console.log('Token Id One:');
console.log(tokenOne);
}
main()
.then(async () => {
await prisma.$disconnect();
})
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
});

View File

@ -5,7 +5,10 @@
"main": "index.js",
"scripts": {
"build": "yarn tsc",
"test": "echo \"Error: no test specified\" && exit 1"
"invoke:build": "yarn build && serverless invoke local --function submitBuildInfo",
"prisma:generate": "npx prisma generate",
"prisma:pull": "npx prisma db pull --force",
"start": "serverless offline"
},
"author": "fleek",
"license": "MIT",
@ -27,7 +30,14 @@
"@middy/core": "^4.2.7",
"@middy/http-json-body-parser": "^4.2.7",
"@middy/http-response-serializer": "^4.2.8",
"@prisma/client": "^4.13.0",
"@types/node": "^18.15.11",
"aws-sdk": "^2.1342.0",
"uuid": "^9.0.0"
"dotenv": "^16.0.3",
"prisma": "^4.13.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"uuid": "^9.0.0",
"web3": "^1.9.0"
}
}

View File

@ -0,0 +1,27 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mongodb"
url = env("DATABASE_URL")
}
model builds {
id String @id @default(auto()) @map("_id") @db.ObjectId
commitHash String
domain String
githubRepository String
ipfsHash String
}
model tokens {
id String @id @default(auto()) @map("_id") @db.ObjectId
commitHash String
domain String
githubRepository String
ipfsHash String
owner String
tokenId Int
verified Boolean
}

View File

@ -31,15 +31,25 @@ custom:
functions:
submitBuildInfo:
handler: dist/functions/builds/handler.submitBuildInfo
handler: src/functions/builds/handler.submitBuildInfo # Change `src` to `dist` for deployment
events:
- http:
path: build
method: post
cors: true
request:
parameters:
querystrings:
githubOrg: true
githubRepo: true
commitHash: true
author: true
timestamp: true
ipfsHash: true
tokenId: true
submitMintInfo:
handler: dist/functions/mints/handler.submitMintInfo
handler: src/functions/mints/handler.submitMintInfo
events:
- http:
path: mint

View File

@ -1,16 +1,92 @@
import { APIGatewayProxyResult } from 'aws-lambda';
import { APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';
import { formatJSONResponse } from '@libs/api-gateway';
import { v4 } from 'uuid';
import { prisma } from '@libs/prisma';
import { account, nfaContract } from '@libs/nfa-contract';
export const submitBuildInfo = async (): Promise<APIGatewayProxyResult> => {
export const submitBuildInfo = async (
event: APIGatewayEvent
): Promise<APIGatewayProxyResult> => {
try {
if (event.body === null) {
return formatJSONResponse({
status: 422,
message: 'Required parameters were not passed.',
});
}
const data = JSON.parse(event.body);
const id = v4();
const buildInfo = {
buildId: id,
createdAt: new Date().toISOString(),
githubRepository: data.githubRepository,
commitHash: data.commitHash,
ipfsHash: data.ipfsHash,
domain: data.domain,
};
// Add build record to the database, if it's not already added
const buildRecord = await prisma.builds.findMany({
where: {
commitHash: buildInfo.commitHash,
githubRepository: buildInfo.githubRepository,
ipfsHash: buildInfo.ipfsHash,
domain: buildInfo.domain,
},
});
if (buildRecord.length == 0) {
await prisma.builds.create({
data: {
githubRepository: buildInfo.githubRepository,
commitHash: buildInfo.commitHash,
ipfsHash: buildInfo.ipfsHash,
domain: buildInfo.domain,
},
});
}
const mintRecord = await prisma.tokens.findMany({
where: {
ipfsHash: buildInfo.ipfsHash,
domain: buildInfo.domain,
commitHash: buildInfo.commitHash,
githubRepository: buildInfo.githubRepository,
verified: false,
},
});
if (mintRecord.length > 0) {
// Trigger verification
// Mark the token as verified in the contract
// call the `setTokenVerified` method
await nfaContract.methods
.setTokenVerified(mintRecord[0].tokenId, true)
.send({
from: account.address,
gas: '1000000',
})
.catch(console.error);
// Update the database record in the tokens collection
await prisma.tokens.updateMany({
where: {
ipfsHash: buildInfo.ipfsHash,
domain: buildInfo.domain,
commitHash: buildInfo.commitHash,
githubRepository: buildInfo.githubRepository,
verified: false,
},
data: {
verified: true,
},
});
}
return formatJSONResponse({
buildInfo,
});

View File

@ -1,45 +1,198 @@
import {
APIGatewayProxyResult,
APIGatewayEvent,
///APIGatewayEventRequestContext,
} from 'aws-lambda';
import { formatJSONResponse } from '@libs/api-gateway';
import { v4 } from 'uuid';
export const submitMintInfo = async (
event: APIGatewayEvent,
///context: APIGatewayEventRequestContext
): Promise<APIGatewayProxyResult> => {
try {
const id = v4();
/**if (!verifyAlchemySig(event.headers.xalchemywork)) {
APIGatewayProxyResult,
APIGatewayEvent,
///APIGatewayEventRequestContext,
} from 'aws-lambda';
import { formatJSONResponse } from '@libs/api-gateway';
import { v4 } from 'uuid';
import { initPrisma, prisma } from '@libs/prisma';
import { account, nfaContract, web3 } from '@libs/nfa-contract';
export const submitMintInfo = async (
event: APIGatewayEvent
///context: APIGatewayEventRequestContext
): Promise<APIGatewayProxyResult> => {
try {
if (event.body === null) {
return formatJSONResponse({
status: 422,
message: 'Required parameters were not passed.',
});
}
const id = v4();
/**if (!verifyAlchemySig(event.headers.xalchemywork)) {
throw new Error('Invalid sig');
}**/
if (event.body == undefined) {
throw new Error('Undefined data');
}
const mintInfo = {
buildId: id,
createdAt: new Date().toISOString(),
body: JSON.parse(event.body),
};
// check if we have it in mongo
// if so, trigger verification call
// if not, add to mongo
return formatJSONResponse({
mintInfo,
});
} catch (e) {
return formatJSONResponse({
status: 500,
message: e,
const eventBody = JSON.parse(event.body);
const topics = eventBody.event.data.block.logs[1].slice(1, 3);
const hexCalldata = eventBody.event.data.block.logs[1].data;
const decodedLogs = web3.eth.abi.decodeLog(
[
{
indexed: true,
internalType: 'uint256',
name: 'tokenId',
type: 'uint256',
},
{
indexed: false,
internalType: 'string',
name: 'name',
type: 'string',
},
{
indexed: false,
internalType: 'string',
name: 'description',
type: 'string',
},
{
indexed: false,
internalType: 'string',
name: 'externalURL',
type: 'string',
},
{
indexed: false,
internalType: 'string',
name: 'ENS',
type: 'string',
},
{
indexed: false,
internalType: 'string',
name: 'commitHash',
type: 'string',
},
{
indexed: false,
internalType: 'string',
name: 'gitRepository',
type: 'string',
},
{
indexed: false,
internalType: 'string',
name: 'ipfsHash',
type: 'string',
},
{
indexed: false,
internalType: 'string',
name: 'logo',
type: 'string',
},
{
indexed: false,
internalType: 'uint24',
name: 'color',
type: 'uint24',
},
{
indexed: false,
internalType: 'bool',
name: 'accessPointAutoApproval',
type: 'bool',
},
{
indexed: true,
internalType: 'address',
name: 'minter',
type: 'address',
},
{
indexed: true,
internalType: 'address',
name: 'owner',
type: 'address',
},
{
indexed: false,
internalType: 'address',
name: 'verifier',
type: 'address',
},
],
hexCalldata,
topics
);
const mintInfo = {
mintId: id,
createdAt: new Date().toISOString(),
tokenId: decodedLogs.tokenId,
githubRepository: decodedLogs.gitRepository,
commit_hash: decodedLogs.commitHash,
owner: decodedLogs.owner,
ipfsHash: decodedLogs.ipfsHash,
domain: decodedLogs.externalURL,
};
initPrisma();
// Check if there is any build associated with the repository, commit hash, tokenId, and ipfsHash
const build = await prisma.builds.findMany({
where: {
githubRepository: mintInfo.githubRepository,
commitHash: mintInfo.commit_hash,
ipfsHash: mintInfo.ipfsHash,
domain: mintInfo.domain,
},
});
let verified = false;
if (build.length > 0) {
// Mark the token as verified in the contract
try {
// call the `setTokenVerified` method
await nfaContract.methods
.setTokenVerified(mintInfo.tokenId, true)
.send({
from: account.address,
gas: '1000000',
});
verified = true;
} catch (error) {
// catch transaction error
console.error(error);
}
}
// Add the record to the database
const token = await prisma.tokens.findMany({
where: {
tokenId: Number(mintInfo.tokenId),
},
});
if (token.length == 0) {
await prisma.tokens.create({
data: {
tokenId: Number(mintInfo.tokenId),
githubRepository: mintInfo.githubRepository,
commitHash: mintInfo.commit_hash,
owner: mintInfo.owner,
ipfsHash: mintInfo.ipfsHash,
verified: verified,
domain: mintInfo.domain,
},
});
}
};
return formatJSONResponse({
mintInfo,
});
} catch (e) {
return formatJSONResponse({
status: 500,
message: e,
});
}
};

View File

@ -10,4 +10,4 @@ export const newMint = {
},
},
],
};
};

View File

@ -0,0 +1,16 @@
var Web3 = require('web3');
var web3 = new Web3(Web3.givenProvider || 'ws://localhost:17895');
export const logDecoder = (
eventFieldsABI: {
indexed: boolean;
internalType: string;
name: string;
type: string;
}[],
data: string,
topics: string[]
) => {
return web3.eth.abi.decodeLog(eventFieldsABI, data, topics);
};

View File

@ -0,0 +1,18 @@
import Web3 from 'web3';
import * as abiFile from '../../../contracts/deployments/goerli/FleekERC721.json';
import * as dotenv from 'dotenv';
dotenv.config();
if (process.env.PRIVATE_KEY === undefined) {
throw Error('Private key environment variable not set.');
}
const contract_address = abiFile.address;
export const abi = abiFile.abi as any;
export const web3 = new Web3('https://rpc.goerli.mudit.blog');
export const nfaContract = new web3.eth.Contract(abi, contract_address);
export const account = web3.eth.accounts.privateKeyToAccount(
process.env.PRIVATE_KEY
);

View File

@ -0,0 +1,18 @@
import { PrismaClient } from '@prisma/client';
export const prisma = new PrismaClient();
export async function initPrisma() {
// Connect the client
await prisma.$connect();
}
initPrisma()
.catch(async (e) => {
console.error(e);
await prisma.$disconnect();
process.exit(1);
})
.finally(() => {
prisma.$disconnect();
});

View File

@ -1,12 +1,19 @@
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"esModuleInterop": true,
"resolveJsonModule": true,
"lib": ["ESNext"],
"moduleResolution": "node",
"noUnusedLocals": true,
"noUnusedParameters": true,
"removeComments": true,
"outDir": "dist",
"target": "es2016",
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@functions/*": ["src/functions/*"],

File diff suppressed because it is too large Load Diff

View File

@ -27,11 +27,12 @@ type NewMint @entity(immutable: true) {
ENS: String!
commitHash: String! # string
gitRepository: String! # string
ipfsHash: String!
logo: String!
color: Int!
accessPointAutoApproval: Boolean!
triggeredBy: Bytes! # address
owner: Owner! # address
owner: Owner! # address
verifier: Bytes!
blockNumber: BigInt!
blockTimestamp: BigInt!
@ -44,7 +45,7 @@ type MetadataUpdate @entity(immutable: true) {
key: String!
stringValue: String
uint24Value: Int
doubleStringValue: [String!]!
multipleStringValue: [String!]!
booleanValue: Boolean
byAddress: Bytes!
blockNumber: BigInt!
@ -76,10 +77,20 @@ type Token @entity {
owner: Owner!
mintedBy: Bytes!
controllers: [Controller!]
accessPoints: [AccessPoint!] @derivedFrom(field: "token")
verifier: Verifier # Address
verified: Boolean!
createdAt: BigInt!
builds: [Build!]!
}
type Build @entity {
id: Bytes! # Token ID
gitRepository: GitRepository!
commitHash: String!
accessPoints: [AccessPoint!] @derivedFrom(field: "token")
verifier: Verifier! # Address
ipfsHash: String!
domain: String!
token: Token! @derivedFrom(field: "builds")
}
# Owner entity for collection, access points, and tokens
@ -104,7 +115,7 @@ type Verifier @entity {
type GitRepository @entity {
id: String! # transaction hash of the first transaction this repository appeared in
tokens: [Token!] @derivedFrom(field: "gitRepository")
builds: [Build!] @derivedFrom(field: "gitRepository")
}
type AccessPoint @entity {
@ -115,4 +126,10 @@ type AccessPoint @entity {
nameVerified: Boolean!
owner: Owner!
creationStatus: String!
}
createdAt: BigInt!
}
type Collection @entity {
id: Bytes!
totalTokens: BigInt!
}

View File

@ -1,4 +1,4 @@
import { Bytes, log } from '@graphprotocol/graph-ts';
import { Bytes, log, store } from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config]
import {
@ -7,15 +7,8 @@ import {
} from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema]
import { Owner, Token } from '../generated/schema';
enum CollectionRoles {
Owner,
}
enum TokenRoles {
Controller,
}
import { Owner, Token, Verifier } from '../generated/schema';
import { CollectionRoles, TokenRoles } from './constants';
export function handleCollectionRoleChanged(
event: CollectionRoleChangedEvent
@ -25,35 +18,56 @@ export function handleCollectionRoleChanged(
const role = event.params.role;
const status = event.params.status;
if (role === CollectionRoles.Owner) {
// Owner role
if (status) {
// granted
let owner = Owner.load(toAddress);
if (!owner) {
owner = new Owner(toAddress);
switch (role) {
case CollectionRoles.Owner:
// Owner role
if (status) {
// granted
let owner = Owner.load(toAddress);
if (!owner) {
owner = new Owner(toAddress);
}
owner.collection = true;
owner.save();
} else {
// revoked
const owner = Owner.load(toAddress);
if (!owner) {
log.error(
'Owner entity not found. Role: {}, byAddress: {}, toAddress: {}',
[role.toString(), byAddress.toHexString(), toAddress.toHexString()]
);
return;
}
owner.collection = false;
owner.save();
}
owner.collection = true;
owner.save();
} else {
// revoked
const owner = Owner.load(toAddress);
if (!owner) {
log.error(
'Owner entity not found. Role: {}, byAddress: {}, toAddress: {}',
[role.toString(), byAddress.toHexString(), toAddress.toHexString()]
);
return;
break;
case CollectionRoles.Verifier:
// Verifier role
if (status) {
// granted
let verifier = Verifier.load(toAddress);
if (!verifier) {
verifier = new Verifier(toAddress);
}
verifier.save();
} else {
// revoked
const verifier = Verifier.load(toAddress);
if (verifier) {
store.remove('Verifier', verifier.id.toString());
}
}
owner.collection = false;
owner.save();
}
} else {
log.error('Role not supported. Role: {}, byAddress: {}, toAddress: {}', [
role.toString(),
byAddress.toHexString(),
toAddress.toHexString(),
]);
break;
default:
log.error('Role not supported. Role: {}, byAddress: {}, toAddress: {}', [
role.toString(),
byAddress.toHexString(),
toAddress.toHexString(),
]);
}
}

View File

@ -29,6 +29,7 @@ export function handleNewAccessPoint(event: NewAccessPointEvent): void {
accessPointEntity.token = Bytes.fromByteArray(
Bytes.fromBigInt(event.params.tokenId)
);
accessPointEntity.createdAt = event.block.timestamp;
// Load / Create an Owner entity
let ownerEntity = Owner.load(event.params.owner);

View File

@ -0,0 +1,8 @@
export enum CollectionRoles {
Owner,
Verifier,
}
export enum TokenRoles {
Controller,
}

View File

@ -1,10 +1,10 @@
import { log, ethereum } from '@graphprotocol/graph-ts';
import { log, ethereum, BigInt, Address } from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config]
import { Initialized as InitializedEvent } from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema]
import { Owner } from '../generated/schema';
import { Collection, Owner, Verifier } from '../generated/schema';
export function handleInitialized(event: InitializedEvent): void {
// This is the contract creation transaction.
log.warning('This is the contract creation transaction.', []);
@ -14,9 +14,18 @@ export function handleInitialized(event: InitializedEvent): void {
receipt.contractAddress.toHexString(),
]);
// start collection entity
const collection = new Collection(event.address);
collection.totalTokens = BigInt.fromU32(0);
collection.save();
// add owner
const owner = new Owner(event.transaction.from);
owner.collection = true;
owner.save();
// add verifier
const verifier = new Verifier(event.transaction.from);
verifier.save();
}
}

View File

@ -2,7 +2,6 @@ import { Bytes } from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config]
import {
MetadataUpdate as MetadataUpdateEvent,
MetadataUpdate1 as MetadataUpdateEvent1,
MetadataUpdate2 as MetadataUpdateEvent2,
MetadataUpdate3 as MetadataUpdateEvent3,
@ -14,6 +13,7 @@ import {
GitRepository as GitRepositoryEntity,
MetadataUpdate,
Token,
Build,
} from '../generated/schema';
export function handleMetadataUpdateWithStringValue(
@ -62,7 +62,7 @@ export function handleMetadataUpdateWithStringValue(
}
}
export function handleMetadataUpdateWithDoubleStringValue(
export function handleMetadataUpdateWithMultipleStringValues(
event: MetadataUpdateEvent3
): void {
/**
@ -74,30 +74,29 @@ export function handleMetadataUpdateWithDoubleStringValue(
entity.key = event.params.key;
entity.tokenId = event.params._tokenId;
entity.doubleStringValue = event.params.value;
entity.multipleStringValue = event.params.value;
entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;
entity.save();
// UPDATE TOKEN
const token = Token.load(
// CREATE BUILD
const build = new Build(
Bytes.fromByteArray(Bytes.fromBigInt(event.params._tokenId))
);
if (token) {
if (event.params.key == 'build') {
let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]);
if (!gitRepositoryEntity) {
// Create a new gitRepository entity
gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]);
}
token.commitHash = event.params.value[0];
token.gitRepository = event.params.value[1];
token.save();
gitRepositoryEntity.save();
if (event.params.key == 'build') {
let gitRepositoryEntity = GitRepositoryEntity.load(event.params.value[1]);
if (!gitRepositoryEntity) {
// Create a new gitRepository entity
gitRepositoryEntity = new GitRepositoryEntity(event.params.value[1]);
}
build.commitHash = event.params.value[0];
build.gitRepository = event.params.value[1];
build.ipfsHash = event.params.value[2];
build.domain = event.params.value[3];
build.save();
gitRepositoryEntity.save();
}
}
@ -159,6 +158,9 @@ export function handleMetadataUpdateWithBooleanValue(
if (event.params.key == 'accessPointAutoApproval') {
token.accessPointAutoApproval = event.params.value;
}
if (event.params.key == 'verified') {
token.verified = event.params.value;
}
token.save();
}
}

View File

@ -1,10 +1,17 @@
import { Bytes, log } from '@graphprotocol/graph-ts';
import { BigInt, Bytes, log } from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config]
import { NewMint as NewMintEvent } from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema]
import { Owner, NewMint, Token } from '../generated/schema';
import {
Owner,
NewMint,
Token,
GitRepository,
Collection,
Verifier,
} from '../generated/schema';
export function handleNewMint(event: NewMintEvent): void {
const newMintEntity = new NewMint(
@ -16,6 +23,7 @@ export function handleNewMint(event: NewMintEvent): void {
const externalURL = event.params.externalURL;
const ENS = event.params.ENS;
const gitRepository = event.params.gitRepository;
const ipfsHash = event.params.ipfsHash;
const commitHash = event.params.commitHash;
const logo = event.params.logo;
const color = event.params.color;
@ -31,6 +39,7 @@ export function handleNewMint(event: NewMintEvent): void {
newMintEntity.ENS = ENS;
newMintEntity.commitHash = commitHash;
newMintEntity.gitRepository = gitRepository;
newMintEntity.ipfsHash = ipfsHash;
newMintEntity.logo = logo;
newMintEntity.color = color;
newMintEntity.accessPointAutoApproval = accessPointAutoApproval;
@ -61,20 +70,37 @@ export function handleNewMint(event: NewMintEvent): void {
token.description = description;
token.externalURL = externalURL;
token.ENS = ENS;
token.gitRepository = gitRepository;
token.commitHash = commitHash;
token.logo = logo;
token.color = color;
token.accessPointAutoApproval = accessPointAutoApproval;
token.owner = ownerAddress;
token.verifier = verifierAddress;
token.verified = false;
token.mintTransaction = event.transaction.hash.concatI32(
event.logIndex.toI32()
);
token.mintedBy = event.params.minter;
token.controllers = [ownerAddress];
token.createdAt = event.block.timestamp;
if (Verifier.load(verifierAddress)) {
token.verifier = verifierAddress;
}
// Populate GitRepository entity
let repository = GitRepository.load(gitRepository);
if (!repository) {
repository = new GitRepository(gitRepository);
}
// Increase total tokens counter
const collection = Collection.load(event.address);
if (collection) {
collection.totalTokens = collection.totalTokens.plus(BigInt.fromU32(1));
collection.save();
}
// Save entities
owner.save();
token.save();
repository.save();
}

View File

@ -1,10 +1,10 @@
import { Bytes, log, store } from '@graphprotocol/graph-ts';
import { BigInt, Bytes, log, store } from '@graphprotocol/graph-ts';
// Event Imports [based on the yaml config]
import { Transfer as TransferEvent } from '../generated/FleekNFA/FleekNFA';
// Entity Imports [based on the schema]
import { Owner, Token, Transfer } from '../generated/schema';
import { Collection, Owner, Token, Transfer } from '../generated/schema';
export function handleTransfer(event: TransferEvent): void {
const transfer = new Transfer(
@ -39,6 +39,14 @@ export function handleTransfer(event: TransferEvent): void {
// Remove the entity from storage
// Its controllers and owner will be affected.
store.remove('Token', TokenId.toString());
// decrement the collection entity's token count
const collection = Collection.load(event.address);
if (collection) {
collection.totalTokens = collection.totalTokens.minus(
BigInt.fromU32(1)
);
collection.save();
}
} else {
// Transfer
// Load the Token by using its TokenId

View File

@ -32,7 +32,7 @@ dataSources:
- ChangeAccessPointAutoApproval
abis:
- name: FleekNFA
file: ../contracts/deployments/goerli/FleekERC721.json
file: ../contracts/artifacts/contracts/FleekERC721.sol/FleekERC721.json
eventHandlers:
- event: Approval(indexed address,indexed address,indexed uint256)
handler: handleApproval
@ -41,13 +41,13 @@ dataSources:
# Token Events
- event: MetadataUpdate(indexed uint256,string,string,indexed address)
handler: handleMetadataUpdateWithStringValue
- event: MetadataUpdate(indexed uint256,string,string[2],indexed address)
handler: handleMetadataUpdateWithDoubleStringValue
- event: MetadataUpdate(indexed uint256,string,string[4],indexed address)
handler: handleMetadataUpdateWithMultipleStringValues
- event: MetadataUpdate(indexed uint256,string,uint24,indexed address)
handler: handleMetadataUpdateWithIntValue
- event: MetadataUpdate(indexed uint256,string,bool,indexed address)
handler: handleMetadataUpdateWithBooleanValue
- event: NewMint(indexed uint256,string,string,string,string,string,string,string,uint24,bool,indexed address,indexed address,address)
- event: NewMint(indexed uint256,string,string,string,string,string,string,string,string,uint24,bool,indexed address,indexed address,address)
handler: handleNewMint
- event: Transfer(indexed address,indexed address,indexed uint256)
handler: handleTransfer

View File

@ -256,13 +256,6 @@
dependencies:
assemblyscript "0.19.10"
"@graphprotocol/graph-ts@^0.27.0":
version "0.27.0"
resolved "https://registry.yarnpkg.com/@graphprotocol/graph-ts/-/graph-ts-0.27.0.tgz#948fe1716f6082964a01a63a19bcbf9ac44e06ff"
integrity sha512-r1SPDIZVQiGMxcY8rhFSM0y7d/xAbQf5vHMWUf59js1KgoyWpM6P3tczZqmQd7JTmeyNsDGIPzd9FeaxllsU4w==
dependencies:
assemblyscript "0.19.10"
"@rescript/std@9.0.0":
version "9.0.0"
resolved "https://registry.yarnpkg.com/@rescript/std/-/std-9.0.0.tgz#df53f3fa5911cb4e85bd66b92e9e58ddf3e4a7e1"
@ -496,7 +489,7 @@ assemblyscript@0.19.10:
binaryen "101.0.0-nightly.20210723"
long "^4.0.0"
assemblyscript@0.19.23, assemblyscript@^0.19.20:
assemblyscript@0.19.23:
version "0.19.23"
resolved "https://registry.yarnpkg.com/assemblyscript/-/assemblyscript-0.19.23.tgz#16ece69f7f302161e2e736a0f6a474e6db72134c"
integrity sha512-fwOQNZVTMga5KRsfY80g7cpOl4PsFQczMwHzdtgoqLXaYhkhavufKb0sB0l3T1DUxpAufA0KNhlbpuuhZUwxMA==
@ -2227,15 +2220,6 @@ mafmt@^7.0.0:
dependencies:
multiaddr "^7.3.0"
matchstick-as@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/matchstick-as/-/matchstick-as-0.5.0.tgz#cdafc1ef49d670b9cbe98e933bc2a5cb7c450aeb"
integrity sha512-4K619YDH+so129qt4RB4JCNxaFwJJYLXPc7drpG+/mIj86Cfzg6FKs/bA91cnajmS1CLHdhHl9vt6Kd6Oqvfkg==
dependencies:
"@graphprotocol/graph-ts" "^0.27.0"
assemblyscript "^0.19.20"
wabt "1.0.24"
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -3399,11 +3383,6 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
wabt@1.0.24:
version "1.0.24"
resolved "https://registry.yarnpkg.com/wabt/-/wabt-1.0.24.tgz#c02e0b5b4503b94feaf4a30a426ef01c1bea7c6c"
integrity sha512-8l7sIOd3i5GWfTWciPL0+ff/FK/deVK2Q6FN+MPz4vfUcD78i2M/49XJTwF6aml91uIiuXJEsLKWMB2cw/mtKg==
wcwidth@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"

View File

@ -25,9 +25,9 @@ query lastNFAsPaginated(
}
}
query totalTokens {
tokens {
id
query totalTokens($contractId: ID!) {
collection(id: $contractId) {
totalTokens
}
}
@ -38,7 +38,7 @@ query getLatestNFAs {
}
}
query getNFA($id: ID!) {
query getNFADetail($id: ID!) {
token(id: $id) {
tokenId
owner {
@ -50,6 +50,33 @@ query getNFA($id: ID!) {
externalURL
logo
color
createdAt
accessPoints {
createdAt
contentVerified
owner {
id
}
id
}
verified
verifier {
id
}
gitRepository {
id
}
}
}
query getNFA($id: ID!) {
token(id: $id) {
tokenId
name
ENS
externalURL
logo
color
}
}

View File

@ -11,7 +11,10 @@
href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;700&display=swap"
rel="stylesheet"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
/>
<meta
name="description"
content="Minimal UI for Sites as NFTs. Fleek XYZ"

View File

@ -7,7 +7,8 @@
"scripts": {
"dev": "vite",
"dev:css": "tailwindcss -o ./tailwind.css --watch && yarn dev",
"build": "yarn graphclient build && vite build",
"build:graph": "yarn graphclient build",
"build": "yarn build:graph && vite build",
"postinstall": "graphclient build",
"preview": "vite preview",
"prod": "yarn build && npx serve dist -s"

View File

@ -2,6 +2,7 @@ import { styled } from '@/theme';
export abstract class CardStyles {
static readonly Container = styled('div', {
width: '$full',
backgroundColor: '$slate2',
borderRadius: '$xlh',
padding: '$7',
@ -10,10 +11,8 @@ export abstract class CardStyles {
borderWidth: '$default',
});
static readonly Heading = styled('h3', {
color: '$slate12',
fontSize: '$xl',
fontWeight: '$medium',
static readonly Header = styled('div', {
width: '$full',
});
static readonly Body = styled('div', {

View File

@ -1,7 +1,6 @@
/* eslint-disable react/display-name */
import React, { forwardRef } from 'react';
import { Flex } from '../layout';
import { CardStyles } from './card.styles';
export abstract class Card {
@ -15,21 +14,7 @@ export abstract class Card {
}
);
static readonly Heading = forwardRef<HTMLHeadingElement, Card.HeadingProps>(
({ title, leftIcon, rightIcon, css, ...props }, ref) => {
return (
<Flex css={{ justifyContent: 'space-between', ...css }}>
<Flex>
{leftIcon}
<CardStyles.Heading ref={ref} {...props}>
{title}
</CardStyles.Heading>
</Flex>
{rightIcon}
</Flex>
);
}
);
static readonly Header = CardStyles.Header;
static readonly Body = forwardRef<HTMLDivElement, Card.BodyProps>(
({ children, ...props }, ref) => {
@ -57,12 +42,7 @@ export namespace Card {
typeof CardStyles.Container
>;
export type HeadingProps = {
title: string;
css?: React.CSSProperties;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
} & React.ComponentProps<typeof CardStyles.Heading>;
export type HeadingProps = React.ComponentProps<typeof CardStyles.Header>;
export type BodyProps = React.ComponentProps<typeof CardStyles.Body>;

View File

@ -0,0 +1,18 @@
import { Card, Flex } from '@/components';
import { styled } from '@/theme';
export const CustomCardStyles = {
Container: styled(Card.Container, {
maxWidth: '$107h',
}),
Title: {
Container: styled(Flex, {
justifyContent: 'space-between',
}),
Text: styled('h3', {
color: '$slate12',
fontSize: '$xl',
fontWeight: '$medium',
}),
},
};

View File

@ -0,0 +1,65 @@
import { Card, Flex, Icon, IconButton } from '@/components';
import { forwardStyledRef } from '@/theme';
import { CardStyles } from '../card.styles';
import { CustomCardStyles as S } from './custom-card.styles';
export const CustomCardContainer = S.Container;
export abstract class CustomCardHeader {
static readonly Default = forwardStyledRef<
HTMLHeadingElement,
CustomCard.HeadingProps
>(({ title, onClickBack, ...props }, ref) => {
return (
<Card.Header ref={ref} {...props}>
<S.Title.Container>
<Flex css={{ gap: '$2' }}>
{onClickBack && (
<IconButton
aria-label="back"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
onClick={onClickBack}
/>
)}
<S.Title.Text>{title}</S.Title.Text>
</Flex>
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
</S.Title.Container>
</Card.Header>
);
});
static readonly Success = forwardStyledRef<
HTMLHeadingElement,
Omit<CustomCard.HeadingProps, 'onClickBack'>
>(({ title, ...props }, ref) => {
return (
<Card.Header ref={ref} {...props}>
<Flex css={{ gap: '$2' }}>
<Icon
name="check-circle"
css={{ color: '$green11', fontSize: '$xl' }}
/>
<S.Title.Text>{title}</S.Title.Text>
</Flex>
</Card.Header>
);
});
}
export namespace CustomCard {
export type ContainerProps = React.ComponentProps<typeof S.Container>;
export type HeadingProps = {
title: string;
onClickBack?: () => void;
} & React.ComponentProps<typeof CardStyles.Header>;
}

View File

@ -0,0 +1 @@
export * from './custom-card';

View File

@ -1 +1,2 @@
export * from './card';
export * from './custom-card';

View File

@ -1,57 +1,23 @@
import React from 'react';
import { ButtonProps } from '.';
import {
StyledButtonContentFlex,
StyledButtonContentGrid,
} from './button-content.styles';
import { ButtonIcon } from './button-icon';
export type ButtonContentProps = Pick<
ButtonProps,
| 'leftIcon'
| 'rightIcon'
| 'topIcon'
| 'bottomIcon'
| 'children'
| 'iconSpacing'
'leftIcon' | 'rightIcon' | 'children'
>;
export const ButtonContent: React.FC<ButtonContentProps> = (props) => {
const {
leftIcon,
rightIcon,
topIcon,
bottomIcon,
children,
iconSpacing = '1h',
} = props;
const { leftIcon, rightIcon, children } = props;
const midNode = (
<>
{leftIcon && (
<ButtonIcon css={{ marginRight: `$${iconSpacing}` }}>
{leftIcon}
</ButtonIcon>
)}
{leftIcon && <ButtonIcon>{leftIcon}</ButtonIcon>}
{children}
{rightIcon && (
<ButtonIcon css={{ marginLeft: `$${iconSpacing}` }}>
{rightIcon}
</ButtonIcon>
)}
{rightIcon && <ButtonIcon>{rightIcon}</ButtonIcon>}
</>
);
if (!topIcon && !bottomIcon) {
return midNode;
}
return (
<StyledButtonContentGrid>
{topIcon && <ButtonIcon>{topIcon}</ButtonIcon>}
<StyledButtonContentFlex>{midNode}</StyledButtonContentFlex>
{bottomIcon && <ButtonIcon>{bottomIcon}</ButtonIcon>}
</StyledButtonContentGrid>
);
return midNode;
};

View File

@ -49,11 +49,6 @@ export interface ButtonProps extends StyledButtonProps {
* @type React.ReactElement
*/
bottomIcon?: React.ReactElement;
/**
* The space between the button icon and label.
* @type SystemProps["marginRight"]
*/
iconSpacing?: string;
/**
* Replace the spinner component when `isLoading` is set to `true`
* @type React.ReactElement
@ -131,17 +126,6 @@ const getButtonCompoundVariant = ({
'&:focus, &:active': {
backgroundColor: `$${color}3`,
},
'&:disabled': {
backgroundColor: `initial`,
'&:hover': {
color: `$${color}11`,
backgroundColor: `initial`,
},
'& img, & svg': {
filter: 'grayscale(100%)',
},
},
};
default:

View File

@ -13,9 +13,6 @@ export const Button = forwardStyledRef<HTMLButtonElement, ButtonProps>(
spinnerPlacement = 'start',
spinner,
loadingText,
iconSpacing,
topIcon,
bottomIcon,
rightIcon,
leftIcon,
isFullWidth,
@ -26,9 +23,6 @@ export const Button = forwardStyledRef<HTMLButtonElement, ButtonProps>(
const contentProps = {
rightIcon,
leftIcon,
bottomIcon,
topIcon,
iconSpacing,
children,
};
@ -40,16 +34,12 @@ export const Button = forwardStyledRef<HTMLButtonElement, ButtonProps>(
data-loading={isLoading}
css={{
width: isFullWidth ? '100%' : undefined,
...(ownProps?.css || {}),
...ownProps?.css,
}}
{...ownProps}
>
{isLoading && spinnerPlacement === 'start' && (
<ButtonSpinner
label={loadingText}
placement={spinnerPlacement}
spacing={iconSpacing}
>
<ButtonSpinner label={loadingText} placement={spinnerPlacement}>
{spinner}
</ButtonSpinner>
)}
@ -65,11 +55,7 @@ export const Button = forwardStyledRef<HTMLButtonElement, ButtonProps>(
)}
{isLoading && spinnerPlacement === 'end' && (
<ButtonSpinner
label={loadingText}
placement={spinnerPlacement}
spacing={iconSpacing}
>
<ButtonSpinner label={loadingText} placement={spinnerPlacement}>
{spinner}
</ButtonSpinner>
)}

View File

@ -8,7 +8,6 @@ type OmittedProps =
| 'isFullWidth'
| 'rightIcon'
| 'loadingText'
| 'iconSpacing'
| 'spinnerPlacement';
type BaseButtonProps = Omit<ButtonProps, OmittedProps>;

View File

@ -62,7 +62,7 @@ export const ColorPicker: React.FC<ColorPickerProps> = ({
</Button>
<input
ref={inputColorRef}
className="absolute right-16"
className="absolute right-16 h-5"
type="color"
value={logoColor}
onChange={handleColorChange}

View File

@ -1,186 +0,0 @@
import { Listbox, Transition } from '@headlessui/react';
import { Fragment } from 'react';
import { Flex } from '@/components';
import { Icon } from '@/components/core/icon';
type DropdownOptionProps = {
option: DropdownItem;
};
const DropdownOption: React.FC<DropdownOptionProps> = ({
option,
}: DropdownOptionProps) => (
<Listbox.Option
className={({ active }) =>
`relative cursor-default select-none py-2 px-3.5 text-slate11 rounded-xl mb-2 text-sm max-w-full ${
active ? 'bg-slate5 text-slate12' : 'bg-transparent'
}`
}
value={option}
>
{({ selected, active }) => (
<Flex css={{ justifyContent: 'space-between' }}>
<span
className={`${
active ? 'text-slate12' : 'text-slate11'
} max-w-full break-words pr-5`}
>
{option.label}
</span>
{selected && (
<Icon
name="check"
color="white"
css={{
position: 'absolute',
top: '$0',
bottom: '$0',
right: '$0',
display: 'flex',
alignItems: 'center',
pr: '$4',
}}
/>
)}
</Flex>
)}
</Listbox.Option>
);
type DropdownButtonProps = {
/**
* The selected value of the dropdown.
*/
selectedValue: DropdownItem | undefined;
/**
* If it's true, the list of options will be displayed
*/
open: boolean;
/**
* Background color of the dropdown. Should be on tailwind palette.
*/
backgroundColor?: string;
/**
* Text color of the dropdown. Should be on tailwind palette.
*/
textColor?: string;
};
const DropdownButton: React.FC<DropdownButtonProps> = ({
selectedValue,
backgroundColor,
textColor,
}: DropdownButtonProps) => {
const textColorCss = textColor ? `text-${textColor}` : 'text-slate12';
const borderColor = backgroundColor
? `border-${backgroundColor}`
: 'border-slate7';
const backgroundColorClass = backgroundColor
? `bg-${backgroundColor}`
: 'bg-transparent';
return (
<Listbox.Button
className={`relative w-full cursor-default ${borderColor} border-solid border rounded-xl py-3 pl-3.5 pr-10 h-11 text-left focus:outline-none sm:text-sm
${backgroundColorClass}
`}
>
<span
className={`block truncate ${
selectedValue && selectedValue.label
? `${textColorCss}`
: 'text-slate11'
} break-words`}
>
{selectedValue && selectedValue.label ? selectedValue.label : 'Select'}
</span>
<span
className={`pointer-events-none absolute top-1 bottom-0 right-0 flex items-center pr-4 ${textColorCss}`}
>
<Icon name="chevron-down" />
</span>
</Listbox.Button>
);
};
export type DropdownItem = {
/**
* The key of the item.
*/
value: string;
/**
* The label to display of the item.
*/
label: string;
};
export type DropdownProps = {
/**
* List of items to be displayed in the dropdown.
*/
items: DropdownItem[];
/**
* The selected value of the dropdown.
*/
selectedValue: DropdownItem | undefined;
/**
* Callback when the selected value changes.
*/
onChange(option: DropdownItem): void;
/**
* Background color of the dropdown. Should be on tailwind palette. https://tailwindcss.com/docs/background-color
*/
backgroundColor?: string;
/**
* Text color of the dropdown. Should be on tailwind palette. https://tailwindcss.com/docs/text-color
*/
textColor?: string;
/**
* Width of the options list. Should be on tailwind width. https://tailwindcss.com/docs/width
*/
optionsWidth?: string;
};
export const Dropdown: React.FC<DropdownProps> = ({
items,
selectedValue,
onChange,
backgroundColor,
textColor,
optionsWidth,
}: DropdownProps) => {
const handleDropdownChange = (option: DropdownItem): void => {
onChange(option);
};
const width = optionsWidth ? `w-${optionsWidth}` : 'w-full';
return (
<Listbox value={selectedValue} by="value" onChange={handleDropdownChange}>
{({ open }) => (
<div className="relative max-w-full">
<DropdownButton
selectedValue={selectedValue}
open={open}
backgroundColor={backgroundColor}
textColor={textColor}
/>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options
className={`absolute mt-1 max-h-36 ${width} right-0 z-10 overflow-auto rounded-xl bg-black px-3 pt-2 border-solid border-slate6 border text-base focus:outline-none sm:text-sm`}
>
{items.map((option: DropdownItem) => (
<DropdownOption key={option.value} option={option} />
))}
</Listbox.Options>
</Transition>
</div>
)}
</Listbox>
);
};

View File

@ -1,2 +1 @@
export * from './dropdown';
export * from './combobox';

View File

@ -4,6 +4,7 @@ import { AiOutlineTwitter } from '@react-icons/all-files/ai/AiOutlineTwitter';
import { BiGitBranch } from '@react-icons/all-files/bi/BiGitBranch';
import { BiSearch } from '@react-icons/all-files/bi/BiSearch';
import { BsFillSquareFill } from '@react-icons/all-files/bs/BsFillSquareFill';
import { FaBars } from '@react-icons/all-files/fa/FaBars';
import { FaChevronRight } from '@react-icons/all-files/fa/FaChevronRight';
import { FaExternalLinkAlt } from '@react-icons/all-files/fa/FaExternalLinkAlt';
import { IoArrowBackCircleSharp } from '@react-icons/all-files/io5/IoArrowBackCircleSharp';
@ -26,7 +27,7 @@ import {
export const IconLibrary = Object.freeze({
back: IoArrowBackCircleSharp,
betaTag: BetaTag,
'beta-tag': BetaTag,
branch: BiGitBranch,
check: AiOutlineCheck,
'check-circle': IoCheckmarkCircleSharp,
@ -36,10 +37,11 @@ export const IconLibrary = Object.freeze({
error: ErrorIcon,
ethereum: EthereumIcon,
'external-link': FaExternalLinkAlt,
fleekLogo: FleekLogo,
fleekName: FleekName,
'fleek-logo': FleekLogo,
'fleek-name': FleekName,
github: IoLogoGithub,
info: IoInformationCircleSharp,
menu: FaBars,
metamask: MetamaskIcon, //remove if not used
search: BiSearch,
square: BsFillSquareFill,

View File

@ -1,9 +1,6 @@
import { styled } from '@/theme';
import { Icon } from '../icon';
const styles = {
all: 'unset',
width: '100%',
boxSizing: 'border-box',
borderStyle: 'solid',
@ -32,7 +29,9 @@ const styles = {
color: '$slate8',
},
},
};
const variants = {
variants: {
size: {
sm: {
@ -59,13 +58,49 @@ const styles = {
},
};
export const InputStyled = styled('input', styles);
export const InputIconStyled = styled(Icon, {
position: 'absolute',
left: '$4',
top: '0.9375rem',
color: 'slate8',
export const InputStyled = styled('input', {
all: 'unset',
...variants,
...styles,
});
export const TextareaStyled = styled('textarea', styles);
export const InputGroupStyled = styled('div', {
display: 'flex',
flexDirection: 'row',
gap: '$2h',
...styles,
variants: {
size: {
sm: {
borderRadius: '$md',
fontSize: '$xs',
lineHeight: '$4',
},
md: {
borderRadius: '$lg',
fontSize: '$sm',
height: '$11',
px: '$3h',
},
lg: {
borderRadius: '$xl',
fontSize: '$md',
},
},
},
defaultVariants: {
size: 'md',
},
});
export const InputGroupTextSyled = styled('input', {
all: 'unset',
});
export const TextareaStyled = styled('textarea', {
all: 'unset',
...variants,
...styles,
});

View File

@ -1,37 +1,17 @@
import React from 'react';
import { forwardStyledRef } from '@/theme';
import { IconName } from '../icon';
import { InputIconStyled, InputStyled, TextareaStyled } from './input.styles';
import {
InputGroupStyled,
InputGroupTextSyled,
InputStyled,
TextareaStyled,
} from './input.styles';
import { StyledInputFile } from './input-file';
export const Textarea = TextareaStyled;
export const LogoFileInput = StyledInputFile;
type InputProps = {
leftIcon?: IconName;
wrapperClassName?: string; //tailwind css
} & React.ComponentPropsWithRef<typeof InputStyled>;
export const Input = InputStyled;
export const Input = forwardStyledRef<HTMLInputElement, InputProps>(
(props, ref) => {
const { leftIcon, wrapperClassName: css = '', ...ownProps } = props;
export const InputGroup = InputGroupStyled;
return (
<div className={`relative ${css}`}>
{leftIcon && (
<InputIconStyled name={leftIcon} css={{ fontSize: '$lg' }} />
)}
<InputStyled
{...props}
ref={ref}
css={{ ...(leftIcon && { pl: '$10' }), ...(ownProps.css || {}) }}
/>
</div>
);
}
);
Input.displayName = 'Input';
export const InputGroupText = InputGroupTextSyled;

View File

@ -1,3 +1,13 @@
import { styled } from '@/theme';
export const Text = styled('span');
export const Text = styled('span', {
variants: {
ellipsis: {
true: {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
},
},
},
});

View File

@ -9,3 +9,4 @@ export * from './nfa-card';
export * from './nfa-preview';
export * from './card-tag';
export * from './resolved-address';
export * from './row-data';

View File

@ -25,7 +25,7 @@ export const ConnectWalletButton: React.FC = () => {
if (ensName && address) setEnsNameStore(ensName, address);
return (
<Button onClick={show}>
<Button onClick={show} css={{ gridArea: 'wallet' }}>
{isConnected && !!address && !!truncatedAddress ? (
<Flex css={{ gap: '$2' }}>
<Avatar address={address} size={20} />

View File

@ -0,0 +1,19 @@
import { useNavigate } from 'react-router-dom';
import { Icon } from '../../core/icon';
import { NavBarStyles as S } from './nav-bar.styles';
export const Logo: React.FC = () => {
const navigate = useNavigate();
return (
<S.Logo.Wrapper onClick={() => navigate('/home')}>
<Icon
name="fleek-logo"
css={{ fontSize: '$2xl' }}
iconElementCss={{ height: '$6' }}
/>
<Icon name="fleek-name" css={{ fontSize: '$6xl', mr: '$3' }} />
<Icon name="beta-tag" css={{ fontSize: '$5xl' }} />
</S.Logo.Wrapper>
);
};

View File

@ -1,33 +1,129 @@
import { styled } from '@/theme';
import { StyledButton } from '@/components/core';
import { alphaColor, styled } from '@/theme';
import { Flex } from '../flex.styles';
export abstract class NavBarStyles {
static readonly Container = styled('header', {
export const NavBarStyles = {
Container: styled('header', {
position: 'sticky',
top: 0,
left: 0,
right: 0,
display: 'flex',
alignItems: 'center',
backgroundColor: '$black',
zIndex: '$sticky',
height: '$22',
overflow: 'hidden', // TODO: this must be worked on for responsive layout
});
static readonly Content = styled('div', {
display: 'flex',
'&:after': {
content: '""',
position: 'absolute',
inset: 0,
backgroundColor: alphaColor('black', 0.8),
backdropFilter: 'blur(4px)',
zIndex: -1,
},
}),
Content: styled('div', {
width: '100%',
maxWidth: '$7xl',
margin: '0 auto',
alignItems: 'center',
padding: '$6',
});
gap: '$3',
static readonly Navigation = styled(Flex, {
gap: '$10',
flexGrow: 4,
justifyContent: 'center',
});
}
display: 'grid',
gridTemplateAreas: '"logo navigation wallet menu"',
gridTemplateColumns: 'auto 1fr auto',
}),
Navigation: {
Container: styled(Flex, {
gridArea: 'navigation',
gap: '$10',
justifyContent: 'center',
fontSize: '$lg',
variants: {
stacked: {
true: {
flexDirection: 'column',
alignItems: 'center',
gap: '$4',
[`${StyledButton}`]: {
fontSize: '$lg',
},
},
},
},
}),
Button: styled(StyledButton, {
variants: {
active: {
true: {
color: '$slate12 !important',
},
},
},
}),
},
Sidebar: {
Content: styled(Flex, {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
position: 'fixed',
top: 0,
bottom: 0,
right: 0,
padding: '$6',
minWidth: '40vw',
zIndex: '$sticky',
backgroundColor: '$black',
transition: 'transform 0.3s ease-in-out',
borderLeft: '1px solid $slate6',
variants: {
open: {
true: {
transform: 'translateX(0%)',
},
false: {
transform: 'translateX(100%)',
},
},
},
}),
Backdrop: styled('div', {
position: 'fixed',
inset: 0,
zIndex: '$sticky',
backgroundColor: alphaColor('black', 0.5),
display: 'none',
transition: 'opacity 0.3s ease-in-out',
variants: {
open: {
true: {
display: 'block',
backdropFilter: 'blur(4px)',
},
},
},
}),
},
Logo: {
Wrapper: styled(Flex, {
gridArea: 'logo',
cursor: 'pointer',
}),
},
};

View File

@ -1,29 +1,20 @@
import { Link } from 'react-router-dom';
import { Button } from '@/components/core';
import { Logo } from '@/components/logo/logo';
import { useMediaQuery } from '@/hooks';
import { ConnectWalletButton } from './connect-wallet-button';
import { Logo } from './logo';
import { NavBarStyles as Styles } from './nav-bar.styles';
import { Navigation } from './navigation';
import { Sidebar } from './sidebar';
export const NavBar: React.FC = () => {
const enableSidebar = useMediaQuery('(max-width: 540px)');
return (
<Styles.Container>
<Styles.Content>
<Logo />
<Styles.Navigation>
<Button as={Link} to="/explore" variant="link" color="gray">
Explore
</Button>
<Button as={Link} to="/mint" variant="link" color="gray">
Create
</Button>
<Button as={Link} to="/" variant="link" color="gray">
Learn
</Button>
</Styles.Navigation>
<ConnectWalletButton />
{enableSidebar ? <Sidebar /> : <Navigation />}
</Styles.Content>
</Styles.Container>
);

View File

@ -0,0 +1,35 @@
import { Link, useLocation } from 'react-router-dom';
import { forwardStyledRef } from '@/theme';
import { NavBarStyles as S } from './nav-bar.styles';
const Paths = [
{ path: '/explore', name: 'Explore', activeRegex: /\/$|\/explore/ },
{ path: '/mint', name: 'Create', activeRegex: /\/mint/ },
{ path: '/', name: 'Learn', activeRegex: /\/learn/ },
];
export const Navigation = forwardStyledRef<
HTMLDivElement,
React.ComponentPropsWithRef<typeof S.Navigation.Container>
>((props, ref) => {
const location = useLocation();
return (
<S.Navigation.Container {...props} ref={ref}>
{Paths.map(({ path, name, activeRegex }) => (
<S.Navigation.Button
key={path}
as={Link}
to={path}
active={activeRegex.test(location.pathname)}
variant="link"
color="gray"
>
{name}
</S.Navigation.Button>
))}
</S.Navigation.Container>
);
});

View File

@ -0,0 +1,45 @@
import { useEffect, useRef, useState } from 'react';
import { Button, Icon } from '@/components/core';
import { NavBarStyles as Styles } from './nav-bar.styles';
import { Navigation } from './navigation';
export const Sidebar: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const sidebarRef = useRef<HTMLDivElement>(null);
const handleToggle = (): void => setIsOpen(!isOpen);
const handleNavigationClick = (): void => setIsOpen(false);
useEffect(() => {
if (!isOpen) return;
const { current } = sidebarRef;
if (!current) return;
const handleClickOutside = (event: MouseEvent): void => {
if (current && !current.contains(event.target as Node)) setIsOpen(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, sidebarRef]);
return (
<>
<Button
onClick={handleToggle}
css={{ gridArea: 'menu', fontSize: '$lg' }}
>
<Icon name="menu" />
</Button>
<Styles.Sidebar.Backdrop open={isOpen} />
<Styles.Sidebar.Content open={isOpen} ref={sidebarRef}>
<Navigation stacked onClick={handleNavigationClick} />
</Styles.Sidebar.Content>
</>
);
};

View File

@ -10,8 +10,11 @@ export abstract class PageStyles {
width: '100%',
minHeight: '85vh',
maxWidth: '$6xl',
padding: '0 $6',
padding: '$6',
margin: '0 auto',
display: 'grid',
'@md': {
padding: '0 $6',
},
});
}

View File

@ -1 +0,0 @@
export * from './logo';

View File

@ -1,15 +0,0 @@
import { styled } from '@/theme';
import { Flex } from '../layout';
export abstract class LogoStyles {
static readonly Container = styled(Flex, {
cursor: 'pointer',
flex: 1,
});
static readonly Logo = styled('img', {
width: '$6',
height: 'auto',
});
}

View File

@ -1,19 +0,0 @@
import { useNavigate } from 'react-router-dom';
import { Icon } from '../core/icon';
import { LogoStyles as LS } from './logo.styles';
export const Logo: React.FC = () => {
const navigate = useNavigate();
return (
<LS.Container onClick={() => navigate('/home')}>
<Icon
name="fleekLogo"
css={{ fontSize: '$2xl' }}
iconElementCss={{ height: '$6' }}
/>
<Icon name="fleekName" css={{ fontSize: '$6xl', mr: '$3' }} />
<Icon name="betaTag" css={{ fontSize: '$5xl' }} />
</LS.Container>
);
};

View File

@ -1,3 +1,4 @@
import { Text } from '@/components';
import { keyframes, styled } from '@/theme';
const Loading = keyframes({
@ -13,7 +14,7 @@ const Loading = keyframes({
});
export const ResolvedAddressStyles = {
Container: styled('span', {
Container: styled(Text, {
'&[data-loading="true"]': {
animation: `${Loading} 1s ease-in-out infinite`,
},

View File

@ -15,7 +15,7 @@ export type ResolvedAddressProps = React.ComponentPropsWithRef<
export const ResolvedAddress = forwardStyledRef<
HTMLSpanElement,
ResolvedAddressProps
>(({ children, truncated = false, ...props }, ref) => {
>(({ children, truncated = true, ...props }, ref) => {
const [resolvedAddress, loading] = useResolvedAddress(children);
const text = useMemo(() => {

View File

@ -0,0 +1 @@
export * from './row-data';

View File

@ -0,0 +1,26 @@
import { Flex, Text } from '@/components';
import { styled } from '@/theme';
import { IconStyles } from '../core/icon/icon.styles';
export const RowDataStyles = {
Container: styled(Flex, {
justifyContent: 'space-between',
}),
Text: {
Container: styled(Flex, {
alignItems: 'center',
maxWidth: '60%',
gap: '$2',
[`${IconStyles.Container}`]: {
fontSize: '$2xl',
},
}),
Label: styled(Text, {
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}),
},
};

View File

@ -0,0 +1,30 @@
import { forwardStyledRef } from '@/theme';
import { RowDataStyles as S } from './row-data.styles';
type RowDataProps = {
leftIcon: React.ReactNode;
label: string;
rightComponent: React.ReactNode;
onClick?: () => void;
};
export const RowData = forwardStyledRef<HTMLDivElement, RowDataProps>(
({ leftIcon, label, rightComponent, onClick, ...props }, ref) => {
const handleOnClick = (): void => {
if (onClick) onClick();
};
return (
<S.Container ref={ref} {...props} onClick={handleOnClick}>
<S.Text.Container>
{leftIcon}
<S.Text.Label>{label}</S.Text.Label>
</S.Text.Container>
{rightComponent}
</S.Container>
);
}
);
RowData.displayName = 'RowData';

View File

@ -0,0 +1,26 @@
import { Flex } from '@/components';
import { styled } from '@/theme';
export const StepStyles = {
Container: styled(Flex, {
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
width: '$full',
gap: '$6',
'@media (min-width: 768px)': {
flexDirection: 'row',
justifyContent: 'center',
},
'@media (min-width: 1024px)': {
gap: '$34',
},
}),
Indicator: styled(Flex, {
flexDirection: 'column',
justifyContent: 'center',
maxWidth: '$106',
}),
};

View File

@ -1,37 +1,6 @@
import { Flex, Stepper } from '@/components';
import { Stepper } from '@/components';
type StepperIndicatorContainerProps = {
children: React.ReactNode;
};
const StepperIndicatorContainer: React.FC<StepperIndicatorContainerProps> = ({
children,
}: StepperIndicatorContainerProps) => {
return (
<Flex
css={{
flexDirection: 'column',
justifyContent: 'center',
mr: '$34',
width: '$106',
}}
>
{children}
</Flex>
);
};
type MintStepContainerProps = {
children: React.ReactNode;
};
const Container: React.FC<MintStepContainerProps> = ({
children,
}: MintStepContainerProps) => (
<Flex css={{ flexDirection: 'row', justifyContent: 'center' }}>
{children}
</Flex>
);
import { StepStyles as S } from './step.styles';
type StepProps = {
children: React.ReactNode;
@ -40,12 +9,12 @@ type StepProps = {
export const Step: React.FC<StepProps> = ({ children, header }: StepProps) => {
return (
<Container>
<StepperIndicatorContainer>
<S.Container>
<S.Indicator>
<Stepper.Indicator />
<h2 className="text-4xl">{header}</h2>
</StepperIndicatorContainer>
</S.Indicator>
{children}
</Container>
</S.Container>
);
};

View File

@ -1,3 +1,4 @@
export * from './use-transaction-cost';
export * from './use-window-scroll-end';
export * from './use-debounce';
export * from './use-media-query';

View File

@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
export const useMediaQuery = (query: string): boolean => {
const getMatches = (query: string): boolean => {
// Prevents SSR issues
if (typeof window !== 'undefined') {
return window.matchMedia(query).matches;
}
return false;
};
const [matches, setMatches] = useState<boolean>(getMatches(query));
const handleChange = (): void => {
setMatches(getMatches(query));
};
useEffect(() => {
const matchMedia = window.matchMedia(query);
// Triggered at the first client-side load and if query changes
handleChange();
// Listen matchMedia
if (matchMedia.addListener) {
matchMedia.addListener(handleChange);
} else {
matchMedia.addEventListener('change', handleChange);
}
return () => {
if (matchMedia.removeListener) {
matchMedia.removeListener(handleChange);
} else {
matchMedia.removeEventListener('change', handleChange);
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
return matches;
};

View File

@ -0,0 +1,9 @@
import { config, theme } from './themes';
config.theme.colors;
export const alphaColor = (
color: keyof typeof theme.colors,
value: number
): string =>
config.theme.colors[color] +
`00${Math.round(0xff * value).toString(16)}`.slice(-2);

View File

@ -23,6 +23,7 @@ export const colors = {
...amber,
};
export const darkColors = {
black: '#000000',
...grayDark,
...slateDark,
...blueDark,

View File

@ -1,5 +1,6 @@
export const media = {
// Breakpoints
xs: '(min-width: 375px)',
sm: '(min-width: 640px)',
md: '(min-width: 768px)',
lg: '(min-width: 1024px)',

View File

@ -10,6 +10,7 @@ export const themeGlobals = globalCss({
fontFamily: 'Manrope',
fontSize: '16px',
'@media (max-width: 850px)': {
fontSize: '13px',
},

View File

@ -2,3 +2,4 @@ export * from './themes';
export * from './foundations';
export * from './key-frames';
export * from './forward-styled-ref';
export * from './alpha-color';

View File

@ -59,7 +59,6 @@ const createDripStitches = <
},
theme: {
colors: {
black: '#000000',
...darkColors, // TODO: replace with light colors once it's done the light mode
...(theme?.colors || {}),
},

View File

@ -10,6 +10,5 @@ export const parseColorToNumber = (color: string): number => {
* Converts string number to hex color string.
*/
export const parseNumberToHexColor = (color: number): string => {
const hexColor = color.toString(16);
return hexColor;
return `${`000000${color.toString(16)}`.slice(-6)}`;
};

View File

@ -10,3 +10,45 @@ export const getRepoAndCommit = (url: string): object => {
export const contractAddress = (address: string): string => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
export const getRepositoryFromURL = (url: string): string => {
const urlSplitted = url.split('/');
return `${urlSplitted[3]}/${urlSplitted[4]}`;
};
export const getDate = (date: number): string => {
return new Date(date * 1000).toLocaleDateString();
};
/**
* @param date date in tiemstamp format
* @returns time since date
*/
export const getTimeSince = (date: number): string => {
const now = new Date().getTime(); //in timestamp format
const milliseconds = now - date * 1000;
const seconds = milliseconds / 1000;
const minutes = Math.round((seconds / 60) % 60);
const days = Math.round(seconds / (60 * 60 * 24));
const hours = Math.round(minutes % 60);
const months = Math.round(days / 30.5);
const years = Math.round(months / 12);
if (years > 0) {
return `${years} year${years > 1 ? 's' : ''} ago`;
}
if (months > 0) {
return `${months} month${months > 1 ? 's' : ''} ago`;
}
if (days > 0) {
return `${days} day${days > 1 ? 's' : ''} ago`;
}
if (hours > 0) {
return `${hours} hour${hours > 1 ? 's' : ''} ago`;
}
if (minutes > 0) {
return `${minutes} min ago`;
}
return `${Math.round(seconds)} sec ago}`;
};

View File

@ -9,6 +9,7 @@ import {
CardTag,
Flex,
Form,
RowData,
Spinner,
Stepper,
Text,
@ -26,25 +27,11 @@ export const SelectedNFA: React.FC = () => {
const { nfa } = CreateAccessPoint.useContext();
return (
<Flex
css={{
justifyContent: 'space-between',
}}
>
<Flex css={{ alignItems: 'center', maxWidth: '65%' }}>
<NFAIconFragment image={nfa.logo} color={nfa.color} />
<Text
css={{
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}
>
{nfa.name}
</Text>
</Flex>
<CardTag css={{ minWidth: '$28' }}>Selected NFA</CardTag>
</Flex>
<RowData
leftIcon={<NFAIconFragment image={nfa.logo} color={nfa.color} />}
label={nfa.name}
rightComponent={<CardTag css={{ minWidth: '$28' }}>Selected NFA</CardTag>}
/>
);
};
@ -77,8 +64,8 @@ export const CreateAccessPointFormBody: React.FC = () => {
},
onCompleted(data) {
if (data.token && id) {
const { name, tokenId, logo, color, externalURL: domain } = data.token;
setNfa({ name, tokenId, logo, color, domain });
const { name, tokenId, logo, color, externalURL } = data.token;
setNfa({ name, tokenId, logo, color, externalURL });
} else {
AppLog.errorToast("We couldn't find the NFA you are looking for");
}

View File

@ -1,4 +1,10 @@
import { Card, Flex, Icon, IconButton, Stepper } from '@/components';
import {
Card,
CustomCardContainer,
CustomCardHeader,
Flex,
Stepper,
} from '@/components';
import { CreateAccessPointFormBody } from './create-ap-form-body';
@ -6,28 +12,8 @@ export const CreateAccessPointForm: React.FC = () => {
const { prevStep } = Stepper.useContext();
return (
<Card.Container css={{ width: '$107h' }}>
<Card.Heading
title="Enter Domain"
leftIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
css={{ mr: '$2' }}
onClick={prevStep}
/>
}
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<CustomCardContainer>
<CustomCardHeader.Default title="Enter Domain" onClickBack={prevStep} />
<Card.Body>
<Flex
css={{
@ -38,6 +24,6 @@ export const CreateAccessPointForm: React.FC = () => {
<CreateAccessPointFormBody />
</Flex>
</Card.Body>
</Card.Container>
</CustomCardContainer>
);
};

View File

@ -1,6 +1,6 @@
import { useEffect, useMemo } from 'react';
import { Button, Card, Grid, SpinnerDot, Stepper, Text } from '@/components';
import { Button, Card, Flex, SpinnerDot, Stepper, Text } from '@/components';
import { bunnyCDNActions, useAppDispatch, useBunnyCDNStore } from '@/store';
import { useAccessPointFormContext } from '../ap-form-step';
@ -49,9 +49,10 @@ export const APRecordCardBody: React.FC = () => {
</Text>
</Card.Text>
) : (
<Grid
<Flex
css={{
rowGap: '$6',
gap: '$6',
flexDirection: 'column',
}}
>
<Text>
@ -73,7 +74,7 @@ export const APRecordCardBody: React.FC = () => {
>
I added the record
</Button>
</Grid>
</Flex>
)}
</Card.Body>
);

View File

@ -1,29 +1,9 @@
import { Card, Icon, IconButton, Stepper } from '@/components';
import { CustomCardHeader, Stepper } from '@/components';
export const APRecordCardHeader: React.FC = () => {
const { prevStep } = Stepper.useContext();
return (
<Card.Heading
title="Create Record"
leftIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="back" />}
css={{ mr: '$2' }}
onClick={prevStep}
/>
}
rightIcon={
<IconButton
aria-label="Add"
colorScheme="gray"
variant="link"
icon={<Icon name="info" />}
/>
}
/>
<CustomCardHeader.Default title="Create Record" onClickBack={prevStep} />
);
};

Some files were not shown because too many files have changed in this diff Show More