Skip to content

Commit

Permalink
wrap up vrf page
Browse files Browse the repository at this point in the history
  • Loading branch information
MattPereira committed Oct 25, 2023
1 parent f671587 commit e1e640c
Show file tree
Hide file tree
Showing 9 changed files with 87 additions and 86 deletions.
22 changes: 14 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ A beginner's guide to implimenting chainlink products in your smart contracts. L
- [🎲 VRF (Verifiable Random Function)](#-vrf-verifiable-random-function)
- [🤖 Automation](#-automation)


## Getting Started

1. Install dependencies
Expand All @@ -33,44 +32,51 @@ yarn start
```

## 📈 Price Feeds
Chainlink price feeds offer a decentralized data source that provides price information for a range of assets pairs depending on the network. The price feeds are powered by a decentralized network of independent, security-reviewed, and Sybil-resistant oracle nodes. The `AggregatorV3Interface` is fixed to the price feed address used during instantiation, but the `FeedRegistry` is more flexible although it is only avaible on mainnet.

Chainlink price feeds offer a decentralized data source that provides price information for a range of assets pairs depending on the network. The price feeds are powered by a decentralized network of independent, security-reviewed, and Sybil-resistant oracle nodes. The `AggregatorV3Interface` is fixed to the price feed address used during instantiation, but the `FeedRegistry` is more flexible although it is only avaible on mainnet.

### Steps

1. Import `AggregatorV3Interface` into your smart contract
2. Declare a state variable of type `AggregatorV3Interface`
3. Choose network and a pair of assets to find the price feed address
3. Choose network and a pair of assets to find the price feed address
4. Instantiate the variable using the price feed address
5. Call `.latestRoundData()` and extract the answer

5. Call `.latestRoundData()` and extract the answer

### Details

- The price returned by the price feed contract has a specified number of decimals that can be fetched from the price feed contract using the `decimals()` method
- The answer returned by the price feed contract is only updated if the price deviates beyond a specified threshold or if a certain amount of time has passed since the last update
- The answer returned by the price feed contract is only updated if the price deviates beyond a specified threshold or if a certain amount of time has passed since the last update
- `FeedRegistry` is only available on Ethereum Mainnet, but `AggregatorV3Interface` is available on a variety of networks.

## 🎲 VRF (Verifiable Random Function)
Chainlink VRF allows a smart contract to access verifiably random numbers. Each request for a random number costs LINK and the reponse is delivered on chain after requestConfirmations number of blocks. The VRFConsumer example uses the Direct Funding method, but you may prefer the Subscription method depending on your use case.

Chainlink VRF provides access verifiably random numbers on chain. Each request for a random number costs LINK and the reponse is delivered on chain after requestConfirmations number of blocks. The VRFConsumer example uses the Direct Funding method, but you may prefer the Subscription method depending on your use case.

### Steps

1. Set up your contract to inherit VRFV2WrapperConsumerBase
2. Impliment a function that triggers request for random number by calling the requestRandomness function which is inhereted from VRFV2WrapperConsumerBase
3. You must override the fullFillrandomWords function

### Details

- The Direct Funding method requires your smart contract hold LINK tokens for payment
- The fulfillRandomWords function is triggered by the VRF Coordinator contract
- VRF response time is impacted by requestConfirmations which must be greater than the minimum amount set by the coordinator contract

## 🤖 Automation

Chainlink Automation calls a smart contract function if a specified set of criteria are met. The time-based trigger calls a target function on a target contract every specified interval. The custom logic trigger allows your contract to use on-chain state to determine when to call a target function. The log trigger allows your contract to use event log data as both a trigger and an input.

### Steps

1. Decide which trigger fits best for your use case
2. Register a new upkeep with chainlink
2. Register a new upkeep with chainlink
3. Provide your target contract and target function

### Details

- The time-based trigger does not require an interface
- The custom logic trigger requires your target contract be compatible with AutomationCompatibleInterface
- The log trigger requires your target contract be compatible with IlogAutomation
11 changes: 7 additions & 4 deletions packages/hardhat/contracts/AggregatorV3Consumer.sol
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

// Import the AggregatorV3Interface from the "@chainlink/contracts" package
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";

/**
* @dev the price feed address passed to constructor during deployment determines the asset pair
* @notice https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1
/** Simple contract for integrating a Chainlink price feed
*
* the price feed address passed to constructor during deployment determines the asset pair
*
* choose a price feed address:
* https://docs.chain.link/data-feeds/price-feeds/addresses?network=ethereum&page=1
*/

contract AggregatorV3Consumer {
// Declare state variable of type AggregatorV3Interface
AggregatorV3Interface internal immutable i_priceFeed;
Expand Down
13 changes: 9 additions & 4 deletions packages/hardhat/contracts/VRFConsumer.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import "@chainlink/contracts/src/v0.8/shared/access/ConfirmedOwner.sol";
*/

contract VRFConsumer is VRFV2WrapperConsumerBase, ConfirmedOwner {
// State variables
// State Variables
address public linkAddress;
uint32 callbackGasLimit = 100000; // limit for gas can be used when chainlink node calls fulfillRandomWords()
uint16 requestConfirmations = 3; // blocks before chainlink node responds (must be greater than a minimum amout set by VRF coordinator contract)
Expand Down Expand Up @@ -37,28 +37,33 @@ contract VRFConsumer is VRFV2WrapperConsumerBase, ConfirmedOwner {
}

/** This function triggers the request to chainlink node that generates the random number
* @dev "requestRandomness()" is inherited from VRFV2WrapperConsumerBase
*
* "requestRandomness()" is inherited from VRFV2WrapperConsumerBase
*
* @return requestId each request has a unique ID
*/

function spinWheel() public returns (uint256 requestId) {
// this request will revert if the contract does not have enough LINK to pay the fee
requestId = requestRandomness(
callbackGasLimit,
requestConfirmations,
numValues
);

// keep track of who sent the request
s_spinners[requestId] = msg.sender;
emit WheelSpun(requestId, msg.sender);
}

/** Chainlink oracle calls this function to deliver the random number
*
* @param requestId The ID of the request
* @param randomWords Array containing the random number(s)
*
* @dev use the random number to change state of your contract here
* @dev the random number is huge so use modulo to constrain the range
* @dev modulo is used to constrain the range of the random number
*/

function fulfillRandomWords(
uint256 requestId,
uint256[] memory randomWords
Expand Down
1 change: 0 additions & 1 deletion packages/nextjs/components/automation/Showcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export const Showcase = () => {
<h3 className="text-2xl md:text-3xl mb-0 font-bold">AutomationConsumer</h3>
<ExternalLinkButton href="https://github.com/MattPereira/speedrun-chainlink/blob/main/packages/hardhat/contracts/AutomationConsumer.sol" />
</div>
{/* <div className="badge badge-warning">{linkBalance?.toString()} LINK</div> */}
<div>
<Address size="xl" address={vrfConsumerContract?.address} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/components/common/InlineCode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ type Props = {

export const InlineCode: React.FC<Props> = ({ text, href }) => {
const content = (
<code className="bg-[#273F66] text-neutral-100 border border-base-200 py-0.5 px-1 rounded-md">{text}</code>
<code className="bg-[#2F487A] text-neutral-200 border border-[#274072] py-0 px-1 rounded-md">{text}</code>
);

return href ? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const AggregatorV3Consumer = () => {
<Address size="xl" address={aggregatorV3Consumer?.address} />
</div>

<p className="text-xl">The latest price quote returned by ETH/USD price feed contract on sepolia</p>
<p className="text-xl">The latest round data returned by ETH/USD price feed contract on sepolia</p>

{!latestPrice || !decimals || !description ? (
<p>Loading...</p>
Expand Down
8 changes: 2 additions & 6 deletions packages/nextjs/components/vrf/ResultsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import { Address } from "../scaffold-eth";
import { ResultType } from "./Showcase";

interface WheelOption {
option: string;
}

interface Result {
spinner: string;
randomValue: number;
}

interface ResultsTableProps {
results: Result[];
results: ResultType[];
wheelOptions: WheelOption[];
}

Expand Down
97 changes: 42 additions & 55 deletions packages/nextjs/components/vrf/Showcase.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import dynamic from "next/dynamic";
import { ResultsTable } from "./ResultsTable";
import { LoaderIcon } from "react-hot-toast";
import { formatEther } from "viem";
import { InformationCircleIcon } from "@heroicons/react/24/outline";
import { Spinner } from "~~/components/assets/Spinner";
import { ExternalLinkButton } from "~~/components/common";
import { InlineCode } from "~~/components/common";
Expand All @@ -19,38 +20,25 @@ import {
} from "~~/hooks/scaffold-eth/";
import { notification } from "~~/utils/scaffold-eth";

// dynamic import to satisfy nextjs ssr
// dynamic import of roulette wheel package to satisfy nextjs ssr
const Wheel = dynamic(() => import("react-custom-roulette").then(mod => mod.Wheel), {
ssr: false,
loading: () => <p>Loading...</p>,
});

type Result = {
spinner: string;
randomValue: number;
export type ResultType = {
spinner: string | undefined;
randomValue: bigint | undefined;
};
/**
*
*/

export const Showcase = () => {
const [mustSpin, setMustSpin] = useState(false);
const [prizeNumber, setPrizeNumber] = useState(0);
// const [isTxPending, setIsTxPending] = useState(false);
const [results, setResults] = useState<Result[]>([]);
const [results, setResults] = useState<ResultType[]>([]);
const [pointerVisibility, setPointerVisibility] = useState<"visible" | "hidden">("visible");
const [waitingForVRF, setWaitingForVRF] = useState(false);
const [notificationId, setNotificationId] = useState<string | null>(null);

const handleSpinClick = async () => {
try {
setPointerVisibility("hidden");
// setWaitingForVRF(true);
await spinWheel();
} catch (e) {
// setWaitingForVRF(false);
}
};

const { data: vrfConsumerContract } = useScaffoldContract({ contractName: "VRFConsumer" });

const { data: linkBalance } = useScaffoldContractRead({
Expand All @@ -70,37 +58,36 @@ export const Showcase = () => {
},
});

const { data: resultsData, isLoading: resultsLoading } = useScaffoldEventHistory({
const { data: resultsData, isLoading: isResultsLoading } = useScaffoldEventHistory({
contractName: "VRFConsumer",
eventName: "WheelResult",
fromBlock: 4491891n,
});
// 4555283

useScaffoldEventSubscriber({
contractName: "VRFConsumer",
eventName: "WheelResult",
listener: logs => {
logs.map(log => {
setWaitingForVRF(false);
const { spinner, randomValue } = log.args;
const randomNum = Number(randomValue);
setWaitingForVRF(false);
setPointerVisibility("visible");
if (!mustSpin) {
setPrizeNumber(randomNum);
setPrizeNumber(Number(randomValue));
setMustSpin(true);
}
notification.success(
<TxnNotification
message={`VRF delivered a ${wheelOptions[randomNum].option}`}
message={`VRF Coordinator delivered a ${wheelOptions[Number(randomValue)].option}`}
blockExplorerLink={log.transactionHash}
/>,
{
duration: 20000,
},
);
// add result to spin events table
if (spinner && randomValue) {
setResults(prev => [{ spinner, randomValue: randomNum }, ...prev]);
setResults(prev => [{ spinner, randomValue }, ...prev]);
console.log("here");
}
});
},
Expand All @@ -117,14 +104,20 @@ export const Showcase = () => {
}, [waitingForVRF, notificationId]);

useEffect(() => {
if (!results.length && !!resultsData?.length && !resultsLoading) {
if (!results.length && !!resultsData?.length && !isResultsLoading) {
setResults(
resultsData?.map(({ args }) => {
return { spinner: args.spinner!, randomValue: Number(args.randomValue!) };
}) || [],
resultsData.map(({ args }) => ({
spinner: args.spinner,
randomValue: args.randomValue,
})) || [],
);
}
}, [resultsLoading, resultsData, results.length]);
}, [isResultsLoading, resultsData, results.length]);

const handleSpinClick = () => {
setPointerVisibility("hidden");
spinWheel();
};

return (
<section>
Expand All @@ -139,42 +132,34 @@ export const Showcase = () => {
</div>
<div className="mb-10">
<p className="text-xl">
Spin the wheel to trigger request to chainlink VRF for a random number. Each request costs{" "}
<InlineCode text="LINK" /> that is paid using the{" "}
<ExternalLink href="https://docs.chain.link/vrf/v2/direct-funding" text="Direct Funding Method" />. After the
request transaction is mined, the VRF Coordinator waits a minimum of{" "}
<InlineCode text="requestConfirmations" /> blocks before responding with a random value.
Spin the wheel to trigger a request for a random number. Each request costs <InlineCode text="LINK" /> that is
paid using the <ExternalLink href="https://docs.chain.link/vrf/v2/direct-funding" text="Direct Funding" />{" "}
method. After the request transaction is mined, the VRF Coordinator waits a minimum of{" "}
<InlineCode text="requestConfirmations" /> blocks before calling the <InlineCode text="fulfillRandomWords" />{" "}
function on the VRFConsumer contract.
</p>
</div>

<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
<div className="flex flex-col">
<h5 className="text-2xl text-center font-bold mb-3">Spin Events</h5>
<div className="bg-base-200 rounded-xl grow">
{!resultsData || resultsLoading ? (
{!resultsData || isResultsLoading ? (
<div className="w-full h-full flex flex-col justify-center items-center">
<Spinner width="75" height="75" />
</div>
) : (
<ResultsTable results={results} wheelOptions={wheelOptions} />
)}
</div>
<div className="alert bg-sky-300 text-primary border-sky-500 mt-5">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="stroke-current shrink-0 w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
{linkBalance ? parseFloat(formatEther(linkBalance)).toFixed(2) : "0.0"} LINK available in contract to pay
for VRF requests
<div className="alert mt-5 text-xl">
<InformationCircleIcon className="stroke-current shrink-0 w-6 h-6" />
<div>
<span className="font-bold mr-2">
{linkBalance ? parseFloat(formatEther(linkBalance)).toFixed(2) : "0.0"} LINK
</span>
available in VRFConsumer to pay for requests
</div>
</div>
</div>
<div>
Expand All @@ -185,7 +170,9 @@ export const Showcase = () => {
prizeNumber={prizeNumber}
data={wheelOptions}
spinDuration={1}
onStopSpinning={() => setMustSpin(false)}
onStopSpinning={() => {
console.log("stopping spinner"), setMustSpin(false);
}}
pointerProps={{ style: { visibility: pointerVisibility } }}
/>
</div>
Expand Down
Loading

0 comments on commit e1e640c

Please sign in to comment.