Skip to content

Commit

Permalink
Add a smart contract and a logo
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexeyKrasnoperov committed Jul 13, 2024
1 parent 9c8bf9d commit 3744afc
Show file tree
Hide file tree
Showing 9 changed files with 4,134 additions and 138 deletions.
142 changes: 142 additions & 0 deletions packages/backend/contracts/SafeCity.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {FunctionsClient} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/FunctionsClient.sol";
import {ConfirmedOwner} from "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
import {FunctionsRequest} from "@chainlink/contracts/src/v0.8/functions/v1_0_0/libraries/FunctionsRequest.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

contract SafeCity is FunctionsClient, ConfirmedOwner {
using FunctionsRequest for FunctionsRequest.Request;

error UnexpectedRequestID(bytes32 requestId);

string source =
"const contractAddress = args[0];"
"const userAddress = args[1];"
"const currentBlock = args[2];"
"const startBlock = currentBlock - 1800;"
"const url = `https://explorer.linea.build/api?module=account&action=txlist&address=${contractAddress}&startblock=${startBlock}&endblock=${currentBlock}&sort=desc&offset=100&page=0&filterBy=to`"
"const apiRequest = Functions.makeHttpRequest({"
"url: url"
"});"
"const apiResponse = await apiRequest;"
"if (apiResponse.error) {"
"throw Error('Request failed');"
"}"
"const data = apiResponse.data;"
"const result = data.result.some(entity => entity.from === userAddress);"
"return Functions.encodeUint256(result == true ? 1 : 0);";

//Callback gas limit
uint32 gasLimit = 300000;

bytes32 donID;
uint64 subscriptionID;

struct Review {
address reviewer;
bool happy;
string review;
}

// Review[] public reviews;
mapping(address reviewedAddress => Review[]) private reviews;

struct FunctionRequestMetadata {
address reviewer;
address reviewedAddress;
bool happy;
string review;
}

mapping(bytes32 => FunctionRequestMetadata) private functionRequestMetadata;

event NewReview(address reviewedAddress, address reviewer, bool happy, string review, bytes error);

/**
* @notice Initializes the contract with the Chainlink router address and sets the contract owner
*/
constructor(
address router,
bytes32 _donID,
uint64 _subscriptionID
) FunctionsClient(router) ConfirmedOwner(msg.sender) {
donID = _donID;
subscriptionID = _subscriptionID;
}

function reportHappiness(address _reviewedAddress, bool happy, string memory review) public {
FunctionsRequest.Request memory req;
req.initializeRequestForInlineJavaScript(source);
string[] memory args = new string[](3);
args[0] = toAsciiString(_reviewedAddress);
args[1] = toAsciiString(msg.sender);
args[2] = Strings.toString(block.number);
req.setArgs(args);

// Send the request and store the request ID
bytes32 requestId = _sendRequest(
req.encodeCBOR(),
subscriptionID,
gasLimit,
donID
);

functionRequestMetadata[requestId] = FunctionRequestMetadata(
msg.sender,
_reviewedAddress,
happy,
review
);
}

function getRecentReviews(address _reviewedAddress) public view returns (Review[] memory) {
return reviews[_reviewedAddress];
}

function toAsciiString(address x) internal pure returns (string memory) {
bytes memory s = new bytes(40);
for (uint i = 0; i < 20; i++) {
bytes1 b = bytes1(uint8(uint(uint160(x)) / (2**(8*(19 - i)))));
bytes1 hi = bytes1(uint8(b) / 16);
bytes1 lo = bytes1(uint8(b) - 16 * uint8(hi));
s[2*i] = char(hi);
s[2*i+1] = char(lo);
}
return string(s);
}

function char(bytes1 b) internal pure returns (bytes1 c) {
if (uint8(b) < 10) return bytes1(uint8(b) + 0x30);
else return bytes1(uint8(b) + 0x57);
}

/**
* @notice Callback function for fulfilling a request
* @param requestId The ID of the request to fulfill
* @param response The HTTP response data
* @param err Any errors from the Functions request
*/
function fulfillRequest(
bytes32 requestId,
bytes memory response,
bytes memory err
) internal override {
if (bytes(functionRequestMetadata[requestId].review).length == 0) {
revert UnexpectedRequestID(requestId);
}

address _reviewer = functionRequestMetadata[requestId].reviewer;
address _reviewedAddress = functionRequestMetadata[requestId].reviewedAddress;
bool _happy = functionRequestMetadata[requestId].happy;
string memory _review = functionRequestMetadata[requestId].review;

if (abi.decode(response, (uint256)) == 1) {
Review memory newReview = Review(_reviewer, _happy, _review);
reviews[_reviewedAddress].push(newReview);
}

emit NewReview(_reviewedAddress, _reviewer, _happy, _review, err);
}
}
24 changes: 24 additions & 0 deletions packages/backend/hardhat.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
require('@nomicfoundation/hardhat-toolbox');

Check failure on line 1 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Imported module should be assigned
require('dotenv').config()

Check failure on line 2 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Insert `;`

module.exports = {
solidity: {

Check failure on line 5 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Replace `↹` with `··`

Check failure on line 5 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Unexpected tab character
version: "0.8.24",

Check failure on line 6 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Replace `↹↹version:·"0.8.24"` with `····version:·'0.8.24'`

Check failure on line 6 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Unexpected tab character
settings: {

Check failure on line 7 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Replace `↹↹` with `····`

Check failure on line 7 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Unexpected tab character
optimizer: {

Check failure on line 8 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Replace `↹↹↹` with `······`

Check failure on line 8 in packages/backend/hardhat.config.js

View workflow job for this annotation

GitHub Actions / Build, lint, and test / Lint

Unexpected tab character
enabled: true
}
}
},
allowUnlimitedContractSize: true,
networks: {
hardhat: {},
ETH_SEPOLIA: {
accounts: [`${process.env.PRIVATE_KEY}`],
url: `https://eth-sepolia.g.alchemy.com/v2/${process.env.ALCHEMY_API_KEY}`
},
},
etherscan: {
apiKey: `${process.env.ETHERSCAN_API_KEY}`
},
}
34 changes: 34 additions & 0 deletions packages/backend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "safe-city-contracts",
"version": "0.1.0",
"private": true,
"scripts": {
"build": "npx hardhat compile",
"deploy-sepolia": "npx hardhat run ./scripts/deploy.js --network ETH_SEPOLIA",
"deploy-linea-sepolia": "npx hardhat run ./scripts/deploy.js --network LINEA_SEPOLIA",
"node": "npx hardhat node",
"deploy-local": "npx hardhat run ./scripts/deploy.js --network localhost"
},
"devDependencies": {
"@chainlink/contracts": "^1.1.1",
"@nomicfoundation/hardhat-chai-matchers": "^2.0.4",
"@nomicfoundation/hardhat-ethers": "^3.0.0",
"@nomicfoundation/hardhat-network-helpers": "^1.0.0",
"@nomicfoundation/hardhat-toolbox": "^4.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.0",
"@openzeppelin/contracts": "^5.0.1",
"@typechain/ethers-v6": "^0.5.0",
"@typechain/hardhat": "^9.0.0",
"@types/chai": "^4.2.0",
"@types/mocha": ">=9.1.0",
"chai": "^4.2.0",
"dotenv": "^16.0.2",
"ethers": "^6.10.0",
"hardhat": "latest",
"hardhat-gas-reporter": "^1.0.8",
"solidity-coverage": "^0.8.1",
"ts-node": ">=8.0.0",
"typechain": "^8.3.0",
"typescript": ">=4.5.0"
}
}
21 changes: 21 additions & 0 deletions packages/backend/scripts/deploy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const { ethers } = require('hardhat');

async function main() {
const DomainOwnership = await ethers.getContractFactory('SafeCity');
const domainOwnership = await DomainOwnership.deploy(
// Sepolia
"0xb83E47C2bC239B3bf370bc41e1459A34b41238D0", // router address - Check to get the router address for your supported network https://docs.chain.link/chainlink-functions/supported-networks
"0x66756e2d657468657265756d2d7365706f6c69612d3100000000000000000000", // donID - Check to get the donID for your supported network https://docs.chain.link/chainlink-functions/supported-networks
3107 // subscription ID
);
await domainOwnership.waitForDeployment();

console.log("DomainOwnership deployed to:", await domainOwnership.getAddress());
}

main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
1 change: 1 addition & 0 deletions packages/snap/images/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
3 changes: 2 additions & 1 deletion packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@
"url": "https://github.com/AlexeyKrasnoperov/contract-traffic-snap.git"
},
"source": {
"shasum": "c3LcB6GY/YC3Mjc5BDebSk9AY/FTlqJ7U5mBNuI3vNM=",
"shasum": "aUKuCiEFb6ItnI6mAdbRQccdgxk/eqREI5V3JfvDO4k=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
"iconPath": "images/icon.svg",
"packageName": "contract-traffic-snap",
"registry": "https://registry.npmjs.org/"
}
Expand Down
110 changes: 62 additions & 48 deletions packages/snap/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,12 @@ import type {
OnUserInputHandler,
OnTransactionHandler
} from '@metamask/snaps-sdk';
import { divider, SnapMethods } from '@metamask/snaps-sdk';
import { UserInputEventType, DialogType, image } from '@metamask/snaps-sdk';
import { UserInputEventType, DialogType } from '@metamask/snaps-sdk';
import { assert } from '@metamask/utils';

import { Counter } from './components';
import { getCurrent, increment } from './utils';
import { panel, text, button } from '@metamask/snaps-sdk';
import { panel, divider, text, button, image } from '@metamask/snaps-sdk';

import { Image } from '@metamask/snaps-sdk/jsx';

import svgIcon from "./vitalik-traffic.jpeg";
import svgIcon from "../images/vitalik-traffic.jpeg";

/**
* Handle incoming JSON-RPC requests from the dapp, sent through the
Expand Down Expand Up @@ -48,15 +43,14 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
method: 'snap_dialog',
params: {
type: DialogType.Alert,
// content: <Image src='https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png />,
content: panel([
image(svgIcon),
text(`Current block: **${currentBlock}**`),
text(`Interactions: **${interactionsCount}**`),
text(`Popularity: **${interactionsCount}**`),
text(`Upvotes: ...`),
text(`Downvotes: ...`),
divider(),
text("After interacting with the contract, please provide feedback. You vote will only count if you interacted with a contract in the last 24 hours."),
text("After interacting with the contract, please provide feedback. Your vote will only count if you interacted with the contract in the last 24 hours."),
button({
value: "I'm happy",
name: "interactive-button",
Expand All @@ -66,7 +60,6 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
name: "interactive-button",
variant: "secondary",
}),

]),
},
});
Expand All @@ -86,48 +79,69 @@ export const onTransaction: OnTransactionHandler = async ({
chainId,
transaction,
}) => {
const count = 1;
const contractAddress = transaction.to;

const interactionsCount = 100;
const blocksInHour = 1800;
const currentBlock = Number(await ethereum.request({
method: "eth_blockNumber",
}) as number);
const apiURL = `https://explorer.linea.build/api?module=account&action=txlist&address=${contractAddress}&startblock=${currentBlock - blocksInHour}&endblock=${currentBlock}&sort=desc&offset=100&page=0`

return await snap.request({
method: 'snap_dialog',
params: {
const response = await fetch(apiURL);
const data = await response.json();
const interactionsCount = data["result"].length;

return {
type: "alert",
content: panel([
text(`Hello, **${transactionOrigin}**!`),
text(`Interactions in the last 24h: **${interactionsCount}**`)
image(svgIcon),
text(`Current block: **${currentBlock}**`),
text(`Popularity: **${interactionsCount}**`),
text(`Happy users: ...`),
text(`Unhappy users: ...`),
divider(),
text("After interacting with the contract, please provide feedback. Your vote will only count if you interacted with the contract in the last 24 hours."),
button({
value: "I'm happy",
name: "interactive-button",
}),
button({
value: "I'm NOT happy",
name: "interactive-button",
variant: "secondary",
}),
]),
},
});
};
}

// /**
// * Handle incoming user events coming from the Snap interface. This handler
// * handles one event:
// *
// * - `increment`: Increment the counter and update the Snap interface with the
// * new count. It is triggered when the user clicks the increment button.
// *
// * @param params - The event parameters.
// * @param params.id - The Snap interface ID where the event was fired.
// * @param params.event - The event object containing the event type, name and
// * value.
// * @see https://docs.metamask.io/snaps/reference/exports/#onuserinput
// */
// export const onUserInput: OnUserInputHandler = async ({ event, id }) => {
// // Since this Snap only has one event, we can assert the event type and name
// // directly.
// assert(event.type === UserInputEventType.ButtonClickEvent);
// assert(event.name === 'increment');
/**
* Handle incoming user events coming from the Snap interface. This handler
* handles one event:
*
* - `increment`: Increment the counter and update the Snap interface with the
* new count. It is triggered when the user clicks the increment button.
*
* @param params - The event parameters.
* @param params.id - The Snap interface ID where the event was fired.
* @param params.event - The event object containing the event type, name and
* value.
* @see https://docs.metamask.io/snaps/reference/exports/#onuserinput
*/
export const onUserInput: OnUserInputHandler = async ({ event, id }) => {
// Since this Snap only has one event, we can assert the event type and name
// directly.
assert(event.type === UserInputEventType.ButtonClickEvent);
assert(event.name === 'increment');

// const count = await increment();
const count = await console.log("CLICKED");

// await snap.request({
// method: 'snap_updateInterface',
// params: {
// id,
// ui: <Counter count={count} />,
// },
// });
// };
await snap.request({
method: 'snap_updateInterface',
params: {
id,
ui: panel([
text("CLICKED")
]),
},
});
};
Loading

0 comments on commit 3744afc

Please sign in to comment.