Merge pull request #1 from 3lLobo/main

The Merge 🌊
This commit is contained in:
Daniel Contreras Salinas
2022-09-19 11:22:09 +02:00
committed by GitHub
24 changed files with 580 additions and 25388 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
**/*.lock
**/package-lock.json
**/*.log

6
.prettierrc Normal file
View File

@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"endOfLine": "auto"
}

View File

@@ -1 +1,21 @@
# zk-2FA
# Zero-Knowledge 2-Factor Authentication 🗝️ (zk-2FA)
The goal of this project is to provide 2FA for EVM compatible blockchains.
We follow a parallel approach for a twofold Authentication solution. The first implements the popular and broadly adopted TOTP 2FA with a trusted validator. The second solution implements a password-generator based zk proof, which is validated onChain providing a zero-trust security level.
Further we provide a dapp to facilitate user-interaction with our smrt-contracts. All dapp interactions can likewise be performed manually per console.
## TOTP 2FA
A picturesque flow-chart of our TOTP 2FA solution:
![TOTP 2FA](res/totpauth.png)
## zk 2FA
**Artworq in the making**
## Contribute
Feedback and contributions are always welcome 🤗
![ethOnlineBanner](res/ethOnlineBanner.png)

3
backend/.gitignore vendored
View File

@@ -9,3 +9,6 @@ typechain-types
cache
artifacts
.debugger/
compiler_config.json
remix-compiler.config.js

10
backend/.prettierignore Normal file
View File

@@ -0,0 +1,10 @@
dist
.vscode
.next
.swc
node_modules
public
.next
out

View File

@@ -1,13 +1,53 @@
# Sample Hardhat Project
# This is the bacqend, the smart-contract crib
This project demonstrates a basic Hardhat use case. It comes with a sample contract, a test for that contract, and a script that deploys that contract.
![crib](https://user-images.githubusercontent.com/25290565/190274993-05c12f02-aa56-4041-af27-67ffda79bcf1.jpg)
Try running some of the following tasks:
We proudly present the TotpAuthenticator.
One step closer to zero trust and one step away from web2.
You can now use 2FA authentication for your business contacts your web-applications or even your IOTs without a centralized database storing your keys and authenticating users. The Blocqchain takes over!
## Cyborg Run 🏃‍♂️
Yarn, remix and hardhat:
```shell
npx hardhat help
npx hardhat test
GAS_REPORT=true npx hardhat test
npx hardhat node
npx hardhat run scripts/deploy.ts
yarn hardhat node
yarn remixd -s . --remix-ide https://remix.ethereum.org
yarn hardhat test
```
## Hashing
How to calculate and submit hash:
convert TOTP (eg. `123456`) to bytes/hex with ethers. Padding left!!!
Then sha256 it and insert `0x` at the start.
That's it, now it should match the sha256 on-chain.
[sha256](https://it-tools.tech/hash-text)
[bytes32](https://web3-type-converter.onbrn.com/)
## Optimism
A blocqchain with free lunch, I mean, free gas! How could we not choose for Optimism?
Contract TotpAuthenticator deployed to Optimism Goerli:
```bash
0xAdF1c645E2bb8C0057537263db6Ae6ECa7085966
# Deployment transaction hash
0x846528416731ddd42e37b8f2dc9fbac24aaf105ebe23d53707a680fc99d68ce0
```
Owner wallet:
```sh
0x369551E7c1D29756e18BA4Ed7f85f2E6663e1e8d
```
[Testnet Explorer](https://blockscout.com/optimism/goerli)
[Faucets](https://optimismfaucet.xyz/)

View File

@@ -0,0 +1,165 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.17;
// Uncomment this line to use console.log
// import 'hardhat/console.sol';
import '@openzeppelin/contracts/access/Ownable.sol';
struct AuthData {
// fist five digits of the 6-digit totp code tail-padded with a zero and finally parsed to a single number
uint256 totp5;
// Teh sha256 hash of the complete code parsed to a single number.
bytes32 totp6hash;
// time-stamp of TOTP code creation.
uint256 time;
}
struct Authentication {
bool isValid;
uint256 time;
address authenticatedAddress;
}
contract TotpAuthenticator is Ownable {
// Counter provides requestId and increments with each request
uint256 public requestCounter;
// Maps a requestId to the requestor/validator and to the auth requested address
mapping(uint256 => address[2]) public requests;
// Maps a requestId to a address and its response.
mapping(uint256 => mapping(address => AuthData)) public responses;
// Maps requestId to completes authentication
// TODO: make this private and create a function to get this value, which initially checks if the requested Id is below the current counter. Otherwise collisions can happen after reset.
mapping(uint256 => Authentication) public completedAuth;
// Events to index with theGraph in order to notify both parties
event EventAuthRequest(address requestor, address target, uint256 requestId);
event EventAuthResponse(
address responder,
uint256 requestId,
AuthData response
);
event EventAuthValid(uint256 requestId, Authentication authentication);
event EventResetContract(uint256 time);
// Create a request for a wallet to authenticate.
function setRequest(address _target) public {
uint256 _currentCount = requestCounter;
requests[_currentCount] = [msg.sender, _target];
requestCounter++;
emit EventAuthRequest(msg.sender, _target, _currentCount);
}
// Submit a repsonse to an authentication request
function setResponse(
uint256 _requestId,
uint256 _totp5,
bytes32 _totp6hash,
uint256 _time
) public {
// require reqId lover than count
require(_requestId < requestCounter, 'ResuestId too high');
require(completedAuth[_requestId].time == 0, 'Request already authorized');
require(
responses[_requestId][msg.sender].totp5 == 0,
'Response already submitted'
);
AuthData memory _authData = AuthData(_totp5, _totp6hash, _time);
responses[_requestId][msg.sender] = _authData;
emit EventAuthResponse(msg.sender, _requestId, _authData);
}
// // The Requestor can get the repsonse data. Preferably though the event indexer graph
// function getResponses(uint256 _requestId, address _responder)
// public
// view
// returns (AuthData memory)
// {
// // Assert that caller created the AuthRequest
// require(isValidator(_requestId), 'U did not submit this request');
// // Don't think it's allowed to return a mapping
// return responses[_requestId][_responder];
// }
// @param _requestId the id of the request
// @_responseAddress the address which submitted the valid response
function authenticate(
uint256 _requestId,
uint256 _lastDigit,
address _responseAddress
) public {
// Assert that caller created the AuthRequest
require(isValidator(_requestId), 'Validation only by requestor');
require(
responses[_requestId][_responseAddress].time > 0,
'No auth response from this wallet'
);
AuthData memory _authData = responses[_requestId][_responseAddress];
bool _isValid = checkHash(_authData.totp5, _lastDigit, _authData.totp6hash);
require(_isValid, 'On-chain validation failed');
Authentication memory authentication = Authentication(
_isValid,
block.timestamp,
_responseAddress
);
completedAuth[_requestId] = authentication;
emit EventAuthValid(_requestId, authentication);
}
// Returns the authentication details for a completed requestId
function getAuthentication(uint256 _requestId)
public
view
returns (Authentication memory)
{
return completedAuth[_requestId];
}
// Reset the contract by deleting all data
function resetAuthenticator() public onlyOwner {
requestCounter = 0;
// TODO: create zero AuthResponse and set the responses[_requestId] = zeroAuthResponse each time a request is initalized.
// How do we empty the mappings?
emit EventResetContract(block.timestamp);
}
// Check if the sender also submitted the request
function isValidator(uint256 _requestId) private view returns (bool) {
return msg.sender == requests[_requestId][0];
}
function toBytes(uint256 x) private pure returns (bytes memory b) {
b = new bytes(32);
assembly {
mstore(add(b, 32), x)
}
}
// Multiply the 5 didgit response by 10 (smiliar to padding with zero) and add the 6st digit. Then compare the hashes.
function checkHash(
uint256 _totp5,
uint256 _lastDigit,
bytes32 _totp6hash
) private pure returns (bool) {
uint256 _totp6 = _totp5 * 10 + _lastDigit;
// console.log('number totp6');
// console.log(_totp6);
// bytes memory bytestotp6 = toBytes(_totp6);
// console.log('bytes of totp6');
// console.logBytes(bytestotp6);
// bytes32 shatotp6 = sha256(toBytes(_totp6));
// console.log('Sha shatotp6');
// console.logBytes32(shatotp6);
// console.log('original _totp6hash');
// console.logBytes32(_totp6hash);
return sha256(toBytes(_totp6)) == _totp6hash;
}
}

View File

@@ -1,8 +1,22 @@
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import { HardhatUserConfig } from 'hardhat/config'
import '@nomicfoundation/hardhat-toolbox'
const config: HardhatUserConfig = {
solidity: "0.8.9",
};
solidity: '0.8.17',
networks: {
// for testnet
'optimism-goerli': {
url: 'https://goerli.optimism.io',
// accounts: [privateKey1, ]
},
// for the local dev environment
'optimism-local': {
url: 'http://localhost:8545',
accounts: [
'0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d',
],
},
},
}
export default config;
export default config

20159
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,27 @@
{
"name": "hardhat-project",
"scripts": {
"prettier": "prettier --write ../ --config ../.prettierrc"
},
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^1.0.3",
"@nomicfoundation/hardhat-network-helpers": "^1.0.6",
"@nomicfoundation/hardhat-toolbox": "^1.0.2",
"hardhat": "^2.11.1"
"@nomiclabs/hardhat-ethers": "^2.1.1",
"@nomiclabs/hardhat-etherscan": "^3.1.0",
"@openzeppelin/contracts": "^4.7.3",
"@remix-project/remixd": "^0.6.6",
"@typechain/hardhat": "^6.1.3",
"chai": "^4.3.6",
"hardhat": "^2.11.1",
"hardhat-gas-reporter": "^1.0.9",
"prettier": "^2.7.1",
"ts-node": "^10.9.1",
"typescript": "^4.8.3"
},
"dependencies": {
"@typechain/ethers-v5": "^10.1.0",
"solidity-coverage": "^0.8.2",
"typechain": "^8.1.0"
}
}

View File

@@ -1,23 +1,34 @@
import { ethers } from "hardhat";
import { ethers } from 'hardhat'
async function main() {
const currentTimestampInSeconds = Math.round(Date.now() / 1000);
const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
const unlockTime = currentTimestampInSeconds + ONE_YEAR_IN_SECS;
const currentTimestampInSeconds = Math.round(Date.now() / 1000)
const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60
const unlockTime = currentTimestampInSeconds + ONE_YEAR_IN_SECS
const lockedAmount = ethers.utils.parseEther("1");
const lockedAmount = ethers.utils.parseEther('1')
const Lock = await ethers.getContractFactory("Lock");
const lock = await Lock.deploy(unlockTime, { value: lockedAmount });
const Lock = await ethers.getContractFactory('Lock')
const lock = await Lock.deploy(unlockTime, { value: lockedAmount })
await lock.deployed();
await lock.deployed()
console.log(`Lock with 1 ETH and unlock timestamp ${unlockTime} deployed to ${lock.address}`);
console.log(
`Lock with 1 ETH and unlock timestamp ${unlockTime} deployed to ${lock.address}`
)
testTotp()
}
async function testTotp() {
const Totp = await ethers.getContractFactory('TotpAuthenticator')
const totp = await Totp.deploy()
await totp.deployed()
console.log(`Totp successfully deployed to ${totp.address}`)
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
console.error(error)
process.exitCode = 1
})

View File

@@ -1,124 +0,0 @@
import { time, loadFixture } from "@nomicfoundation/hardhat-network-helpers";
import { anyValue } from "@nomicfoundation/hardhat-chai-matchers/withArgs";
import { expect } from "chai";
import { ethers } from "hardhat";
describe("Lock", function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployOneYearLockFixture() {
const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60;
const ONE_GWEI = 1_000_000_000;
const lockedAmount = ONE_GWEI;
const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS;
// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners();
const Lock = await ethers.getContractFactory("Lock");
const lock = await Lock.deploy(unlockTime, { value: lockedAmount });
return { lock, unlockTime, lockedAmount, owner, otherAccount };
}
describe("Deployment", function () {
it("Should set the right unlockTime", async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture);
expect(await lock.unlockTime()).to.equal(unlockTime);
});
it("Should set the right owner", async function () {
const { lock, owner } = await loadFixture(deployOneYearLockFixture);
expect(await lock.owner()).to.equal(owner.address);
});
it("Should receive and store the funds to lock", async function () {
const { lock, lockedAmount } = await loadFixture(
deployOneYearLockFixture
);
expect(await ethers.provider.getBalance(lock.address)).to.equal(
lockedAmount
);
});
it("Should fail if the unlockTime is not in the future", async function () {
// We don't use the fixture here because we want a different deployment
const latestTime = await time.latest();
const Lock = await ethers.getContractFactory("Lock");
await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith(
"Unlock time should be in the future"
);
});
});
describe("Withdrawals", function () {
describe("Validations", function () {
it("Should revert with the right error if called too soon", async function () {
const { lock } = await loadFixture(deployOneYearLockFixture);
await expect(lock.withdraw()).to.be.revertedWith(
"You can't withdraw yet"
);
});
it("Should revert with the right error if called from another account", async function () {
const { lock, unlockTime, otherAccount } = await loadFixture(
deployOneYearLockFixture
);
// We can increase the time in Hardhat Network
await time.increaseTo(unlockTime);
// We use lock.connect() to send a transaction from another account
await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
"You aren't the owner"
);
});
it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
const { lock, unlockTime } = await loadFixture(
deployOneYearLockFixture
);
// Transactions are sent using the first signer by default
await time.increaseTo(unlockTime);
await expect(lock.withdraw()).not.to.be.reverted;
});
});
describe("Events", function () {
it("Should emit an event on withdrawals", async function () {
const { lock, unlockTime, lockedAmount } = await loadFixture(
deployOneYearLockFixture
);
await time.increaseTo(unlockTime);
await expect(lock.withdraw())
.to.emit(lock, "Withdrawal")
.withArgs(lockedAmount, anyValue); // We accept any value as `when` arg
});
});
describe("Transfers", function () {
it("Should transfer the funds to the owner", async function () {
const { lock, unlockTime, lockedAmount, owner } = await loadFixture(
deployOneYearLockFixture
);
await time.increaseTo(unlockTime);
await expect(lock.withdraw()).to.changeEtherBalances(
[owner, lock],
[lockedAmount, -lockedAmount]
);
});
});
});
});

120
backend/tests/Lock.ts Normal file
View File

@@ -0,0 +1,120 @@
import { time, loadFixture } from '@nomicfoundation/hardhat-network-helpers'
import { anyValue } from '@nomicfoundation/hardhat-chai-matchers/withArgs'
import { expect } from 'chai'
import { ethers } from 'hardhat'
describe('Lock', function () {
// We define a fixture to reuse the same setup in every test.
// We use loadFixture to run this setup once, snapshot that state,
// and reset Hardhat Network to that snapshot in every test.
async function deployOneYearLockFixture() {
const ONE_YEAR_IN_SECS = 365 * 24 * 60 * 60
const ONE_GWEI = 1_000_000_000
const lockedAmount = ONE_GWEI
const unlockTime = (await time.latest()) + ONE_YEAR_IN_SECS
// Contracts are deployed using the first signer/account by default
const [owner, otherAccount] = await ethers.getSigners()
const Lock = await ethers.getContractFactory('Lock')
const lock = await Lock.deploy(unlockTime, { value: lockedAmount })
return { lock, unlockTime, lockedAmount, owner, otherAccount }
}
describe('Deployment', function () {
it('Should set the right unlockTime', async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture)
expect(await lock.unlockTime()).to.equal(unlockTime)
})
it('Should set the right owner', async function () {
const { lock, owner } = await loadFixture(deployOneYearLockFixture)
expect(await lock.owner()).to.equal(owner.address)
})
it('Should receive and store the funds to lock', async function () {
const { lock, lockedAmount } = await loadFixture(deployOneYearLockFixture)
expect(await ethers.provider.getBalance(lock.address)).to.equal(
lockedAmount
)
})
it('Should fail if the unlockTime is not in the future', async function () {
// We don't use the fixture here because we want a different deployment
const latestTime = await time.latest()
const Lock = await ethers.getContractFactory('Lock')
await expect(Lock.deploy(latestTime, { value: 1 })).to.be.revertedWith(
'Unlock time should be in the future'
)
})
})
describe('Withdrawals', function () {
describe('Validations', function () {
it('Should revert with the right error if called too soon', async function () {
const { lock } = await loadFixture(deployOneYearLockFixture)
await expect(lock.withdraw()).to.be.revertedWith(
"You can't withdraw yet"
)
})
it('Should revert with the right error if called from another account', async function () {
const { lock, unlockTime, otherAccount } = await loadFixture(
deployOneYearLockFixture
)
// We can increase the time in Hardhat Network
await time.increaseTo(unlockTime)
// We use lock.connect() to send a transaction from another account
await expect(lock.connect(otherAccount).withdraw()).to.be.revertedWith(
"You aren't the owner"
)
})
it("Shouldn't fail if the unlockTime has arrived and the owner calls it", async function () {
const { lock, unlockTime } = await loadFixture(deployOneYearLockFixture)
// Transactions are sent using the first signer by default
await time.increaseTo(unlockTime)
await expect(lock.withdraw()).not.to.be.reverted
})
})
describe('Events', function () {
it('Should emit an event on withdrawals', async function () {
const { lock, unlockTime, lockedAmount } = await loadFixture(
deployOneYearLockFixture
)
await time.increaseTo(unlockTime)
await expect(lock.withdraw())
.to.emit(lock, 'Withdrawal')
.withArgs(lockedAmount, anyValue) // We accept any value as `when` arg
})
})
describe('Transfers', function () {
it('Should transfer the funds to the owner', async function () {
const { lock, unlockTime, lockedAmount, owner } = await loadFixture(
deployOneYearLockFixture
)
await time.increaseTo(unlockTime)
await expect(lock.withdraw()).to.changeEtherBalances(
[owner, lock],
[lockedAmount, -lockedAmount]
)
})
})
})
})

View File

@@ -0,0 +1,49 @@
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
// This import is automatically injected by Remix
import "remix_tests.sol";
// This import is required to use custom transaction context
// Although it may fail compilation in 'Solidity Compiler' plugin
// But it will work fine in 'Solidity Unit Testing' plugin
import "remix_accounts.sol";
import "../contracts/TotpAuthenticator.sol";
// File name has to end with '_test.sol', this file can contain more than one testSuite contracts
contract testSuite {
/// 'beforeAll' runs before all other tests
/// More special functions are: 'beforeEach', 'beforeAll', 'afterEach' & 'afterAll'
function beforeAll() public {
// <instantiate contract>
Assert.equal(uint(1), uint(1), "1 should be equal to 1");
}
function checkSuccess() public {
// Use 'Assert' methods: https://remix-ide.readthedocs.io/en/latest/assert_library.html
Assert.ok(2 == 2, 'should be true');
Assert.greaterThan(uint(2), uint(1), "2 should be greater than to 1");
Assert.lesserThan(uint(2), uint(3), "2 should be lesser than to 3");
}
function checkSuccess2() public pure returns (bool) {
// Use the return value (true or false) to test the contract
return true;
}
function checkFailure() public {
Assert.notEqual(uint(1), uint(1), "1 should not be equal to 1");
}
/// Custom Transaction Context: https://remix-ide.readthedocs.io/en/latest/unittesting.html#customization
/// #sender: account-1
/// #value: 100
function checkSenderAndValue() public payable {
// account index varies 0-9, value is in wei
Assert.equal(msg.sender, TestsAccounts.getAccount(1), "Invalid sender");
Assert.equal(msg.value, 100, "Invalid value");
}
}

10
dapp/.prettierignore Normal file
View File

@@ -0,0 +1,10 @@
dist
.vscode
.next
.swc
node_modules
public
.next
out

View File

@@ -2,7 +2,8 @@ import { useEthers } from '@usedapp/core'
import { useState } from 'react'
import { motion } from 'framer-motion'
import { ConnectWalletButton, TotpSetup, ZkPasswordSetup, CardChoice } from './'
import { ConnectWalletButton, TotpSetup, ZkPasswordSetup, CardChoice } from '.'
const LogInBox = () => {
const { account, library: provider } = useEthers()

View File

@@ -1,5 +1,5 @@
import { useEthers } from '@usedapp/core'
import { DropdownAccount, ToggleColorMode } from './'
import { DropdownAccount, ToggleColorMode } from '.'
const Navbar = () => {
const { account } = useEthers()

View File

@@ -13,3 +13,4 @@ export { default as BoxPendingTransaction } from './BoxPendingTransaction'
export { default as BoxAuthSystem } from './BoxAuthSystem'
export { default as BoxSocialRecovery } from './BoxSocialRecovery'
export { default as ModalSetSocial } from './ModalSetSocial'

View File

@@ -3,10 +3,10 @@ const nextConfig = {
reactStrictMode: true,
swcMinify: true,
webpack: (config) => {
config.resolve.fallback = { fs: false };
config.resolve.fallback = { fs: false }
return config;
}
return config
},
}
module.exports = nextConfig

View File

@@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"prettier": "prettier --write . --config ../.prettierrc"
},
"dependencies": {
"@headlessui/react": "^1.7.1",
@@ -28,6 +29,10 @@
"web3modal": "^1.9.9"
},
"devDependencies": {
"@tailwindcss/aspect-ratio": "^0.4.2",
"@tailwindcss/forms": "^0.5.3",
"@tailwindcss/line-clamp": "^0.4.2",
"@tailwindcss/typography": "^0.5.7",
"@types/node": "18.7.16",
"@types/qrcode": "^1.5.0",
"@types/react": "18.0.19",
@@ -36,6 +41,7 @@
"eslint": "8.23.0",
"eslint-config-next": "12.3.0",
"postcss": "^8.4.16",
"prettier": "^2.7.1",
"tailwindcss": "^3.1.8",
"typescript": "4.8.3"
}

View File

@@ -1,12 +1,76 @@
/** @type {import('tailwindcss').Config} */
const plugin = require('tailwindcss/plugin')
module.exports = {
darkMode: 'class',
content: [
"./pages/**/*.{js,ts,jsx,tsx}",
"./components/**/*.{js,ts,jsx,tsx}",
'./pages/**/*.{js,ts,jsx,tsx}',
'./components/**/*.{js,ts,jsx,tsx}',
],
theme: {
extend: {},
extend: {
scale: {
500: '5',
300: '3',
},
animation: {
'spin-bezier': 'myspin 1s cubic-bezier(0.9, 0.26, 0.97, 1) infinite',
},
keyframes: {
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' },
},
},
colors: {
blocqpurple: '#B88DFF',
neonPurple: 'rgba(111,76,255,1.0)',
navy: '#0b3a53',
'navy-muted': '#244e66',
aqua: '#69c4cd',
'aqua-muted': '#9ad4db',
ipfsgray: '#b7bbc8',
'ipfsgray-muted': '#d9dbe2',
charcoal: '#34373f',
'charcoal-muted': '#7f8491',
ipfsred: '#ea5037',
'ipfsred-muted': '#f36149',
ipfsyellow: '#f39021',
'ipfsyellow-muted': '#f9a13e',
ipfsteal: '#378085',
'ipfsteal-muted': '#439a9d',
ipfsgreen: '#0cb892',
'ipfsgreen-muted': '#0aca9f',
snow: '#edf0f4',
'snow-muted': '#f7f8fa',
link: '#117eb3',
'washed-blue': '#F0F6FA',
},
},
},
plugins: [],
plugins: [
require('@tailwindcss/typography'),
require('@tailwindcss/line-clamp'),
require('@tailwindcss/aspect-ratio'),
require('@tailwindcss/forms'),
plugin(function ({ addUtilities }) {
addUtilities({
'.scrollbar-hide': {
/* IE and Edge */
'-ms-overflow-style': 'none',
/* Firefox */
'scrollbar-width': 'none',
/* Safari and Chrome */
'&::-webkit-scrollbar': {
display: 'none',
},
},
})
}),
plugin(function ({ addComponents }) {
addComponents({})
}),
],
}

File diff suppressed because it is too large Load Diff

BIN
res/ethOnlineBanner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
res/totpauth.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB