initial setup of truffle project

This commit is contained in:
PatrickAlphaC
2020-10-23 15:35:12 -04:00
parent 777b8b46e6
commit c148a9785a
12 changed files with 23053 additions and 2 deletions

101
README.md
View File

@@ -1,2 +1,99 @@
# dynamic-nft
#chainlink #nft
# Chainlink Truffle Box
Implementation of a [Chainlink requesting contract](https://docs.chain.link/docs/create-a-chainlinked-project).
## Requirements
- NPM
## Installation
Package installation should have occurred for you during the Truffle Box setup. However, if you add dependencies, you'll need to add them to the project by running:
```bash
npm install
```
Or
```bash
yarn install
```
## Test
```bash
npm test
```
## Deploy
If needed, edit the `truffle-config.js` config file to set the desired network to a different port. It assumes any network is running the RPC port on 8545.
```bash
npm run migrate:dev
```
For deploying to live networks, Truffle will use `truffle-hdwallet-provider` for your mnemonic and an RPC URL. Set your environment variables `$RPC_URL` and `$MNEMONIC` before running:
```bash
npm run migrate:live
```
## Helper Scripts
There are 3 helper scripts provided with this box in the scripts directory:
- `fund-contract.js`
- `request-data.js`
- `read-contract.js`
They can be used by calling them from `npx truffle exec`, for example:
```bash
npx truffle exec scripts/fund-contract.js --network live
```
The CLI will output something similar to the following:
```
Using network 'live'.
Funding contract: 0x972DB80842Fdaf6015d80954949dBE0A1700705E
0xd81fcf7bfaf8660149041c823e843f0b2409137a1809a0319d26db9ceaeef650
Truffle v5.0.25 (core: 5.0.25)
Node v10.16.3
```
In the `request-data.js` script, example parameters are provided for you. You can change the oracle address, Job ID, and parameters based on the information available on [our documentation](https://docs.chain.link/docs/testnet-oracles).
```bash
npx truffle exec scripts/request-data.js --network live
```
This creates a request and will return the transaction ID, for example:
```
Using network 'live'.
Creating request on contract: 0x972DB80842Fdaf6015d80954949dBE0A1700705E
0x828f256109f22087b0804a4d1a5c25e8ce9e5ac4bbc777b5715f5f9e5b181a4b
Truffle v5.0.25 (core: 5.0.25)
Node v10.16.3
```
After creating a request on a live network, you will want to wait 3 blocks for the Chainlink node to respond. Then call the `read-contract.js` script to read the contract's state.
```bash
npx truffle exec scripts/read-contract.js --network live
```
Once the oracle has responded, you will receive a value similar to the one below:
```
Using network 'live'.
21568
Truffle v5.0.25 (core: 5.0.25)
Node v10.16.3
```

23
contracts/Migrations.sol Normal file
View File

@@ -0,0 +1,23 @@
pragma solidity >=0.4.24 <0.7.0;
contract Migrations {
address public owner;
uint256 public last_completed_migration;
modifier restricted() {
if (msg.sender == owner) _;
}
constructor() public {
owner = msg.sender;
}
function setCompleted(uint256 completed) public restricted {
last_completed_migration = completed;
}
function upgrade(address new_address) public restricted {
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
}
}

108
contracts/MyContract.sol Normal file
View File

@@ -0,0 +1,108 @@
pragma solidity 0.6.6;
import "@chainlink/contracts/src/v0.6/ChainlinkClient.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
/**
* @title MyContract is an example contract which requests data from
* the Chainlink network
* @dev This contract is designed to work on multiple networks, including
* local test networks
*/
contract MyContract is ChainlinkClient, Ownable {
uint256 public data;
/**
* @notice Deploy the contract with a specified address for the LINK
* and Oracle contract addresses
* @dev Sets the storage for the specified addresses
* @param _link The address of the LINK token contract
*/
constructor(address _link) public {
if (_link == address(0)) {
setPublicChainlinkToken();
} else {
setChainlinkToken(_link);
}
}
/**
* @notice Returns the address of the LINK token
* @dev This is the public implementation for chainlinkTokenAddress, which is
* an internal method of the ChainlinkClient contract
*/
function getChainlinkToken() public view returns (address) {
return chainlinkTokenAddress();
}
/**
* @notice Creates a request to the specified Oracle contract address
* @dev This function ignores the stored Oracle contract address and
* will instead send the request to the address specified
* @param _oracle The Oracle contract address to send the request to
* @param _jobId The bytes32 JobID to be executed
* @param _url The URL to fetch data from
* @param _path The dot-delimited path to parse of the response
* @param _times The number to multiply the result by
*/
function createRequestTo(
address _oracle,
bytes32 _jobId,
uint256 _payment,
string memory _url,
string memory _path,
int256 _times
)
public
onlyOwner
returns (bytes32 requestId)
{
Chainlink.Request memory req = buildChainlinkRequest(_jobId, address(this), this.fulfill.selector);
req.add("url", _url);
req.add("path", _path);
req.addInt("times", _times);
requestId = sendChainlinkRequestTo(_oracle, req, _payment);
}
/**
* @notice The fulfill method from requests created by this contract
* @dev The recordChainlinkFulfillment protects this function from being called
* by anyone other than the oracle address that the request was sent to
* @param _requestId The ID that was generated for the request
* @param _data The answer provided by the oracle
*/
function fulfill(bytes32 _requestId, uint256 _data)
public
recordChainlinkFulfillment(_requestId)
{
data = _data;
}
/**
* @notice Allows the owner to withdraw any LINK balance on the contract
*/
function withdrawLink() public onlyOwner {
LinkTokenInterface link = LinkTokenInterface(chainlinkTokenAddress());
require(link.transfer(msg.sender, link.balanceOf(address(this))), "Unable to transfer");
}
/**
* @notice Call this method if no response is received within 5 minutes
* @param _requestId The ID that was generated for the request to cancel
* @param _payment The payment specified for the request to cancel
* @param _callbackFunctionId The bytes4 callback function ID specified for
* the request to cancel
* @param _expiration The expiration generated for the request to cancel
*/
function cancelRequest(
bytes32 _requestId,
uint256 _payment,
bytes4 _callbackFunctionId,
uint256 _expiration
)
public
onlyOwner
{
cancelChainlinkRequest(_requestId, _payment, _callbackFunctionId, _expiration);
}
}

View File

@@ -0,0 +1,5 @@
const Migrations = artifacts.require('Migrations')
module.exports = deployer => {
deployer.deploy(Migrations)
}

View File

@@ -0,0 +1,23 @@
const MyContract = artifacts.require('MyContract')
const { LinkToken } = require('@chainlink/contracts/truffle/v0.4/LinkToken')
const { Oracle } = require('@chainlink/contracts/truffle/v0.6/Oracle')
module.exports = async (deployer, network, [defaultAccount]) => {
// Local (development) networks need their own deployment of the LINK
// token and the Oracle contract
if (!network.startsWith('live')) {
LinkToken.setProvider(deployer.provider)
Oracle.setProvider(deployer.provider)
try {
await deployer.deploy(LinkToken, { from: defaultAccount })
await deployer.deploy(Oracle, LinkToken.address, { from: defaultAccount })
await deployer.deploy(MyContract, LinkToken.address)
} catch (err) {
console.error(err)
}
} else {
// For live networks, use the 0 address to allow the ChainlinkRegistry
// contract automatically retrieve the correct address for you
deployer.deploy(MyContract, '0x0000000000000000000000000000000000000000')
}
}

22431
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

31
package.json Normal file
View File

@@ -0,0 +1,31 @@
{
"name": "@chainlink/box",
"version": "0.6.0",
"description": "A Chainlink example in a Truffle box",
"scripts": {
"compile": "npx truffle compile",
"console:dev": "npx truffle console --network cldev",
"console:live": "npx truffle console --network live",
"depcheck": "echo '@chainlink/box' && depcheck --ignore-dirs=build/contracts || true",
"solhint": "solhint ./contracts/**/*.sol",
"lint": "yarn solhint",
"migrate:dev": "npx truffle migrate --reset --network cldev",
"migrate:live": "npx truffle migrate --network live",
"test": "NODE_ENV=test npx truffle test"
},
"license": "MIT",
"dependencies": {
"@chainlink/contracts": "^0.0.8",
"@openzeppelin/contracts": "^3.1.0",
"@truffle/hdwallet-provider": "^1.0.40"
},
"devDependencies": {
"@chainlink/belt": "^0.0.1",
"@chainlink/test-helpers": "0.0.5",
"@openzeppelin/test-helpers": "^0.5.6",
"chai": "^4.2.0",
"depcheck": "^0.9.1",
"solhint": "^2.1.0",
"truffle": "^5.1.5"
}
}

24
scripts/fund-contract.js Normal file
View File

@@ -0,0 +1,24 @@
const MyContract = artifacts.require('MyContract')
const LinkTokenInterface = artifacts.require('LinkTokenInterface')
/*
This script is meant to assist with funding the requesting
contract with LINK. It will send 1 LINK to the requesting
contract for ease-of-use. Any extra LINK present on the contract
can be retrieved by calling the withdrawLink() function.
*/
const payment = process.env.TRUFFLE_CL_BOX_PAYMENT || '1000000000000000000'
module.exports = async callback => {
try {
const mc = await MyContract.deployed()
const tokenAddress = await mc.getChainlinkToken()
const token = await LinkTokenInterface.at(tokenAddress)
console.log('Funding contract:', mc.address)
const tx = await token.transfer(mc.address, payment)
callback(tx.tx)
} catch (err) {
callback(err)
}
}

12
scripts/read-contract.js Normal file
View File

@@ -0,0 +1,12 @@
const MyContract = artifacts.require('MyContract')
/*
This script makes it easy to read the data variable
of the requesting contract.
*/
module.exports = async callback => {
const mc = await MyContract.deployed()
const data = await mc.data.call()
callback(data)
}

33
scripts/request-data.js Normal file
View File

@@ -0,0 +1,33 @@
const MyContract = artifacts.require('MyContract')
/*
This script allows for a Chainlink request to be created from
the requesting contract. Defaults to the Chainlink oracle address
on this page: https://docs.chain.link/docs/testnet-oracles
*/
const oracleAddress =
process.env.TRUFFLE_CL_BOX_ORACLE_ADDRESS ||
'0xc99B3D447826532722E41bc36e644ba3479E4365'
const jobId =
process.env.TRUFFLE_CL_BOX_JOB_ID || '3cff0a3524694ff8834bda9cf9c779a1'
const payment = process.env.TRUFFLE_CL_BOX_PAYMENT || '1000000000000000000'
const url =
process.env.TRUFFLE_CL_BOX_URL ||
'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD'
const path = process.env.TRUFFLE_CL_BOX_JSON_PATH || 'USD'
const times = process.env.TRUFFLE_CL_BOX_TIMES || '100'
module.exports = async callback => {
const mc = await MyContract.deployed()
console.log('Creating request on contract:', mc.address)
const tx = await mc.createRequestTo(
oracleAddress,
web3.utils.toHex(jobId),
payment,
url,
path,
times,
)
callback(tx.tx)
}

236
test/MyContract_test.js Normal file
View File

@@ -0,0 +1,236 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { oracle } = require('@chainlink/test-helpers')
const { expectRevert, time } = require('@openzeppelin/test-helpers')
contract('MyContract', accounts => {
const { LinkToken } = require('@chainlink/contracts/truffle/v0.4/LinkToken')
const { Oracle } = require('@chainlink/contracts/truffle/v0.6/Oracle')
const MyContract = artifacts.require('MyContract.sol')
const defaultAccount = accounts[0]
const oracleNode = accounts[1]
const stranger = accounts[2]
const consumer = accounts[3]
// These parameters are used to validate the data was received
// on the deployed oracle contract. The Job ID only represents
// the type of data, but will not work on a public testnet.
// For the latest JobIDs, visit our docs here:
// https://docs.chain.link/docs/testnet-oracles
const jobId = web3.utils.toHex('4c7b7ffb66b344fbaa64995af81e355a')
const url =
'https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=USD,EUR,JPY'
const path = 'USD'
const times = 100
// Represents 1 LINK for testnet requests
const payment = web3.utils.toWei('1')
let link, oc, cc
beforeEach(async () => {
link = await LinkToken.new({ from: defaultAccount })
oc = await Oracle.new(link.address, { from: defaultAccount })
cc = await MyContract.new(link.address, { from: consumer })
await oc.setFulfillmentPermission(oracleNode, true, {
from: defaultAccount,
})
})
describe('#createRequest', () => {
context('without LINK', () => {
it('reverts', async () => {
await expectRevert.unspecified(
cc.createRequestTo(oc.address, jobId, payment, url, path, times, {
from: consumer,
}),
)
})
})
context('with LINK', () => {
let request
beforeEach(async () => {
await link.transfer(cc.address, web3.utils.toWei('1', 'ether'), {
from: defaultAccount,
})
})
context('sending a request to a specific oracle contract address', () => {
it('triggers a log event in the new Oracle contract', async () => {
const tx = await cc.createRequestTo(
oc.address,
jobId,
payment,
url,
path,
times,
{ from: consumer },
)
request = oracle.decodeRunRequest(tx.receipt.rawLogs[3])
assert.equal(oc.address, tx.receipt.rawLogs[3].address)
assert.equal(
request.topic,
web3.utils.keccak256(
'OracleRequest(bytes32,address,bytes32,uint256,address,bytes4,uint256,uint256,bytes)',
),
)
})
})
})
})
describe('#fulfill', () => {
const expected = 50000
const response = web3.utils.padLeft(web3.utils.toHex(expected), 64)
let request
beforeEach(async () => {
await link.transfer(cc.address, web3.utils.toWei('1', 'ether'), {
from: defaultAccount,
})
const tx = await cc.createRequestTo(
oc.address,
jobId,
payment,
url,
path,
times,
{ from: consumer },
)
request = oracle.decodeRunRequest(tx.receipt.rawLogs[3])
await oc.fulfillOracleRequest(
...oracle.convertFufillParams(request, response, {
from: oracleNode,
gas: 500000,
}),
)
})
it('records the data given to it by the oracle', async () => {
const currentPrice = await cc.data.call()
assert.equal(
web3.utils.padLeft(web3.utils.toHex(currentPrice), 64),
web3.utils.padLeft(expected, 64),
)
})
context('when my contract does not recognize the request ID', () => {
const otherId = web3.utils.toHex('otherId')
beforeEach(async () => {
request.id = otherId
})
it('does not accept the data provided', async () => {
await expectRevert.unspecified(
oc.fulfillOracleRequest(
...oracle.convertFufillParams(request, response, {
from: oracleNode,
}),
),
)
})
})
context('when called by anyone other than the oracle contract', () => {
it('does not accept the data provided', async () => {
await expectRevert.unspecified(
cc.fulfill(request.requestId, response, { from: stranger }),
)
})
})
})
describe('#cancelRequest', () => {
let request
beforeEach(async () => {
await link.transfer(cc.address, web3.utils.toWei('1', 'ether'), {
from: defaultAccount,
})
const tx = await cc.createRequestTo(
oc.address,
jobId,
payment,
url,
path,
times,
{ from: consumer },
)
request = oracle.decodeRunRequest(tx.receipt.rawLogs[3])
})
context('before the expiration time', () => {
it('cannot cancel a request', async () => {
await expectRevert(
cc.cancelRequest(
request.requestId,
request.payment,
request.callbackFunc,
request.expiration,
{ from: consumer },
),
'Request is not expired',
)
})
})
context('after the expiration time', () => {
beforeEach(async () => {
await time.increase(300)
})
context('when called by a non-owner', () => {
it('cannot cancel a request', async () => {
await expectRevert.unspecified(
cc.cancelRequest(
request.requestId,
request.payment,
request.callbackFunc,
request.expiration,
{ from: stranger },
),
)
})
})
context('when called by an owner', () => {
it('can cancel a request', async () => {
await cc.cancelRequest(
request.requestId,
request.payment,
request.callbackFunc,
request.expiration,
{ from: consumer },
)
})
})
})
})
describe('#withdrawLink', () => {
beforeEach(async () => {
await link.transfer(cc.address, web3.utils.toWei('1', 'ether'), {
from: defaultAccount,
})
})
context('when called by a non-owner', () => {
it('cannot withdraw', async () => {
await expectRevert.unspecified(cc.withdrawLink({ from: stranger }))
})
})
context('when called by the owner', () => {
it('transfers LINK to the owner', async () => {
const beforeBalance = await link.balanceOf(consumer)
assert.equal(beforeBalance, '0')
await cc.withdrawLink({ from: consumer })
const afterBalance = await link.balanceOf(consumer)
assert.equal(afterBalance, web3.utils.toWei('1', 'ether'))
})
})
})
})

28
truffle-config.js Normal file
View File

@@ -0,0 +1,28 @@
const HDWalletProvider = require('@truffle/hdwallet-provider')
module.exports = {
networks: {
cldev: {
host: '127.0.0.1',
port: 8545,
network_id: '*',
},
ganache: {
host: '127.0.0.1',
port: 7545,
network_id: '*',
},
kovan: {
provider: () => {
return new HDWalletProvider(process.env.MNEMONIC, process.env.KOVAN_RPC_URL)
},
network_id: '42',
skipDryRun: true,
},
},
compilers: {
solc: {
version: '0.6.6',
},
},
}