In the last chapter, we created the Fundraiser contract and implemented the functionality to support creating donations and withdrawing funds. The Fundraiser contract has a lot of functionality, but currently it has no means of being created. In this chapter, we are going to add the contract that will create the individual instances of Fundraiser using a pattern that is likely familiar: the Factory pattern.
At the end of the chapter, we will also walk through grabbing the UI from the GitHub repo and deploying to Ganache. Once it has been deployed, we can launch the app in our browser, create new fundraisers, make donations, and withdraw the funds.
Our Fundraiser contract from the last chapter was designed with the idea that it would be initialized from another contract, the FundraiserFactory, and therefore did not require its own migration. The FundraiserFactory, however, will need to be deployed so that our users can interact with the factory and create their own fundraisers.
Create a new file in the test directory using the following command:
$
touchtest
/fundraiser_factory_test.js
Let’s add a deployment test case to our new file:
const
FundraiserFactoryContract
=
artifacts
.
require
(
"FundraiserFactory"
);
contract
(
"FundraiserFactory: deployment"
,
()
=>
{
it
(
"has been deployed"
,
async
()
=>
{
const
fundraiserFactory
=
FundraiserFactoryContract
.
deployed
();
assert
(
fundraiserFactory
,
"fundraiser factory was not deployed"
);
});
});
Running our tests should throw an error with the following message:
Error: Could not find artifacts for FundraiserFactory from any sources
In order to get past this error, we’ll need to define an empty contract with the FundraiserFactory name. Let’s create the contracts/FundraiserFactory.sol file and then define our contract, as illustrated next:
$
touch contracts/FundraiserFactory.sol
pragma solidity
>
0
.
4
.
23
<
0
.
7
.
0
;
contract
FundraiserFactory
{
}
Running our tests now will produce the following output:
$
truffletest
Using network 'test'.
Compiling your contracts...
>
Compiling ./contracts/Fundraiser.sol>
Compiling ./contracts/FundraiserFactory.solContract: FundraiserFactory: deployment
1) has been deployed
> No events were emitted
...omitted...
19 passing (3s)
1 failing
1) Contract: FundraiserFactory: deployment
has been deployed:
Error: FundraiserFactory has not been deployed to detected network
...omitted...
This failure means it is time to write a migration. Create the migration file with the following command:
touch migrations/2_deploy_fundraiser_factory.js
Then add the following deployment code to the file:
const
FundraiserFactoryContract
=
artifacts
.
require
(
"FundraiserFactory"
);
module
.
exports
=
function
(
deployer
)
{
deployer
.
deploy
(
FundraiserFactoryContract
);
}
With the migration in place, let’s run our tests:
$
truffletest
Using network 'test'.
Compiling your contracts...
>
Compiling ./contracts/Fundraiser.sol>
Compiling ./contracts/FundraiserFactory.solContract: FundraiserFactory: deployment
✓ has been deployed
Contract: Fundraiser
...omitted...
16 passing (3s)
With our deployment test out of the way, we can begin working on the primary purpose of our contract: creating Fundraisers.
The FundraiserFactory’s primary job is to create new instances of Fundraisers. In order to do this, we will need to add a function that can initialize them on behalf of our users. The createFundraiser
function we are about to create will take the data entered in the new page shown in Figure 6-2 to create a new Fundraiser and then store a reference to it to allow pagination of all the Fundraisers available.
The observable behaviors that we can test against are that the fundraisersCount
will increment and a LogNewFundraiserCreated
event will be emitted.
Back in test/fundraiser_factory_test.js, let’s add the test illustrated in Example 7-1.
contract
(
"FundraiserFactory: createFundraiser"
,
(
accounts
)
=>
{
let
fundraiserFactory
;
// fundraiser args
const
name
=
"Beneficiary Name"
;
const
url
=
"beneficiaryname.org"
;
const
imageURL
=
"https://placekitten.com/600/350"
const
bio
=
"Beneficiary Description"
const
beneficiary
=
accounts
[
1
];
it
(
"increments the fundraisersCount"
,
async
()
=>
{
fundraiserFactory
=
await
FundraiserFactoryContract
.
deployed
();
const
currentFundraisersCount
=
await
fundraiserFactory
.
fundraisersCount
();
await
fundraiserFactory
.
createFundraiser
(
name
,
url
,
imageURL
,
description
,
beneficiary
);
const
newFundraisersCount
=
await
fundraiserFactory
.
fundraisersCount
();
assert
.
equal
(
newFundraisersCount
-
currentFundraisersCount
,
1
,
"should increment by 1"
)
});
In our new test, we created another contract block. Remember, Truffle will deploy a new instance of our contract when we use the contract function; this will prevent state from being maintained between test groups and prevent ordering issues on test runs. In our contract block, we set variables to hold onto the fundraiser data that would be entered into the form.
In the test, we retrieve the deployed instance, and like our counter tests in the last chapter, we save the current count (currentFundraisersCount
), create a new fundraiser, and then check the count again. We expect the count to have increased by 1.
Running our test will generate the following output:
$
truffletest
Using network 'test'.
Compiling your contracts...
===========================
>
Compiling ./contracts/Fundraiser.sol>
Compiling ./contracts/FundraiserFactory.solContract: FundraiserFactory: deployment
✓ has been deployed
Contract: FundraiserFactory: createFundraiser
1) increments the fundraisersCount
> No events were emitted
Contract: Fundraiser
...omitted...
20 passing (3s)
1 failing
1) Contract: FundraiserFactory: createFundraiser
increments the fundraisersCount:
TypeError: fundraiserFactory.fundraisersCount is not a function
at Context.it (test/fundraiser_factory_test.js:22:61)
at web3.eth.getBlockNumber.then.result (/usr/local/lib/node_modules/...
at process._tickCallback (internal/process/next_tick.js:68:7)
Our test is letting us know we do not have the fundraisersCount
function set up. In order to get past this error, we will need to set up a new state variable on our contract to store the fundraisers that have been created. Then we’ll need to create a function to retrieve the length of the collection. The code in Example 7-2 will get us past this first part.
pragma solidity
>
0
.
4
.
23
<
0
.
7
.
0
;
import
"./Fundraiser.sol"
;
contract
FundraiserFactory
{
Fundraiser
[]
private
_fundraisers
;
function
fundraisersCount
()
public
view
returns
(
uint256
)
{
return
_fundraisers
.
length
;
}
}
Running our tests again, we should see a similar error as before, but this time the createFundraiser
function will be the source of our trouble. Let’s create this function, as illustrated next:
function
createFundraiser
(
string
memory
name
,
string
memory
url
,
string
memory
imageURL
,
string
memory
description
,
address
payable
beneficiary
)
public
{
Fundraiser
fundraiser
=
new
Fundraiser
(
name
,
url
,
imageURL
,
description
,
beneficiary
,
msg
.
sender
);
_fundraisers
.
push
(
fundraiser
);
}
Looking at this code, we are taking in all the parameters from the form and then initializing a new fundraiser. This is the first time we have created a new instance of a contract from a contract! When initializing contracts, we will receive back the address of the new instance, and then we place that in our Fundraisers array.
Running our tests, we should see everything pass:
$
truffletest
Using network 'test'.
Compiling your contracts...
>
Compiling ./contracts/Fundraiser.sol>
Compiling ./contracts/FundraiserFactory.solContract: FundraiserFactory: deployment
✓ has been deployed
Contract: FundraiserFactory: createFundraiser
✓ increments the fundraisersCount (213ms)
Contract: Fundraiser
...omitted...
21 passing (3s)
With that test passing, we can move on to testing that the FundraiserCreated
event has been emitted. We will start by adding the following test:
it
(
"emits the FundraiserCreated event"
,
async
()
=>
{
fundraiserFactory
=
await
FundraiserFactoryContract
.
deployed
();
const
tx
=
await
fundraiserFactory
.
createFundraiser
(
name
,
url
,
imageURL
,
description
,
beneficiary
);
const
expectedEvent
=
"FundraiserCreated"
;
const
actualEvent
=
tx
.
logs
[
0
].
event
;
assert
.
equal
(
actualEvent
,
expectedEvent
,
"events should match"
);
});
Running our tests will generate the following failure:
$
truffletest
Using network 'test'.
Compiling your contracts...
...omitted...
Contract: FundraiserFactory: createFundraiser
✓ increments the fundraisersCount (216ms)
1) emits the FundraiserCreated event
...omitted...
Contract: Fundraiser
...omitted...
21 passing (3s)
1 failing
1) Contract: FundraiserFactory: createFundraiser
emits the FundraiserCreated event:
TypeError: Cannot read property 'event' of undefined
at Context.it (test/fundraiser_factory_test.js:49:36)
at process._tickCallback (internal/process/next_tick.js:68:7)
In order to pass this test, we’ll need to define a new event and then emit it from the createFundraiser
function. The event will be called FundraiserCreated
and will include the address of the new fundraiser and the address of the owner. Let’s update our FundraiserFactory contract to look like Example 7-3.
pragma solidity
>
0
.
4
.
23
<
0
.
7
.
0
;
import
"./Fundraiser.sol"
;
contract
FundraiserFactory
{
Fundraiser
[]
private
_fundraisers
;
event
FundraiserCreated
(
Fundraiser
indexed
fundraiser
,
address
indexed
owner
);
function
createFundraiser
(
string
memory
name
,
string
memory
url
,
string
memory
imageURL
,
string
memory
bio
,
address
payable
beneficiary
)
public
{
Fundraiser
fundraiser
=
new
Fundraiser
(
name
,
url
,
imageURL
,
description
,
beneficiary
,
msg
.
sender
);
_fundraisers
.
push
(
fundraiser
);
emit
FundraiserCreated
(
fundraiser
);
}
function
fundraisersCount
()
public
view
returns
(
uint256
)
{
return
_fundraisers
.
length
;
}
}
Running our test, we should now see it pass! With creation of Fundraisers completed, we will turn our attention to getting data out of the Fundraisers collection. This next bit of functionality will support the bottom of the home page shown in Figure 6-1.
In order for our users to find the fundraisers that have been created, we need a way of displaying the options. One approach would be to get all of the fundraisers created in a single call to our contract, which is similar to what we did for the myDonations
function in our Fundraiser contract. That approach works great when we expect the collection sizes to remain relatively small, but if our app gains popularity, we are going to want to break the loading of this data into chunks. Even though viewing data from the blockchain is free, meaning it doesn’t cost gas, it is still using resources on the EVM. If a user comes to our application and sees the fundraiser they want right away, we end up wasting a lot of those resources. The smaller chunks will also allow our page to render faster than waiting for all the items to be returned from a single request.
We are going to borrow from our SQL friends and create a function that will take limit and offset parameters. We will also add a maxLimit
constant to our FundraiserFactory contract to cap the number of records that can be requested at a time. In other words, if a request comes in asking for 50 records but our cap is 20, they will receive only 20 records. In the case where a user request includes an offset that is greater than the fundraiserCount
, we will exit with an out-of-bounds error.
Because of the number of scenarios we will need to cover in our tests, we will create a helper method to initialize a new instance of our FundraiserFactory contract instead of using the deployed
instance, and then create a passed-in number of fundraisers.
When we first deploy our FundraiserFactory, the collection of Fundraisers will be empty. That seems like the perfect starting case for our tests.
The code in Example 7-4 includes our setup and the first test.
contract
(
"FundraiserFactory: fundraisers"
,
(
accounts
)
=>
{
async
function
createFundraiserFactory
(
fundraiserCount
,
accounts
)
{
const
factory
=
await
FundraiserFactoryContract
.
new
();
await
addFundraisers
(
factory
,
fundraiserCount
,
accounts
);
return
factory
;
}
async
function
addFundraisers
(
factory
,
count
,
accounts
)
{
const
name
=
"Beneficiary"
;
const
lowerCaseName
=
name
.
toLowerCase
();
const
beneficiary
=
accounts
[
1
];
for
(
let
i
=
0
;
i
<
count
;
i
++
)
{
await
factory
.
createFundraiser
(
// create a series of fundraisers. The index will be used
// to make them each unique
`
${
name
}
${
i
}
`
,
`
${
lowerCaseName
}${
i
}
.com`
,
`
${
lowerCaseName
}${
i
}
.png`
,
`Description for
${
name
}
${
i
}
`
,
beneficiary
);
}
}
describe
(
"when fundraisers collection is empty"
,
()
=>
{
it
(
"returns an empty collection"
,
async
()
=>
{
const
factory
=
await
createFundraiserFactory
(
0
,
accounts
);
const
fundraisers
=
await
factory
.
fundraisers
(
10
,
0
);
assert
.
equal
(
fundraisers
.
length
,
0
,
"collection should be empty"
);
});
});
});
Reviewing this setup, our createFundraiserFactory
function creates a new instance of the FundraiserFactory and then passes that into the addFundraisers
function. The addFundraisers
function then creates the passed-in count of fundraisers.
Our first test is the empty collection scenario. When we run this test, we should get a test failure indicating the fundraisers
function doesn’t exist, like in the output here:
$
truffletest
Using network 'test'.
...omitted...
Contract: FundraiserFactory: fundraisers
when fundraisers collection is empty
1) returns an empty collection
> No events were emitted
...omitted...
1 failing
1) Contract: FundraiserFactory: fundraisers
when fundraisers collection is empty
returns an empty collection:
TypeError: factory.fundraisers is not a function
at Context.it (test/fundraiser_factory_test.js:86:41)
at process._tickCallback (internal/process/next_tick.js:68:7)
Let’s add the function to our contract, returning the empty collection. The code is illustrated in Example 7-5.
function
fundraisers
(
uint256
limit
,
uint256
offset
)
public
view
returns
(
Fundraiser
[]
memory
coll
)
{
return
coll
;
}
Our tests should all be passing again, but we still have a ways to go before we can say our function is behaving correctly. Let’s jump into our next group of tests, which are focused on how many results will come back.
For the next few tests, we are going to keep our offset at 0. We will create 30 fundraisers and will vary our limit, which is the number of items we want to receive back. If we ask for 10, we’ll get 10 results back; asking for 20 will get us 20; asking for 30 will get us 20…wait, what? Remember, we mentioned that we’ll put a cap on the number of items that can be requested—we’ll set our limit to 20. These tests are illustrated in Example 7-6.
describe
(
"varying limits"
,
async
()
=>
{
let
factory
;
beforeEach
(
async
()
=>
{
factory
=
await
createFundraiserFactory
(
30
,
accounts
);
})
it
(
"returns 10 results when limit requested is 10"
,
async
()
=>
{
const
fundraisers
=
await
factory
.
fundraisers
(
10
,
0
);
assert
.
equal
(
fundraisers
.
length
,
10
,
"results size should be 10"
);
});
// xit marks the test as pending
xit
(
"returns 20 results when limit requested is 20"
,
async
()
=>
{
const
fundraisers
=
await
factory
.
fundraisers
(
20
,
0
);
assert
.
equal
(
fundraisers
.
length
,
20
,
"results size should be 20"
);
});
xit
(
"returns 20 results when limit requested is 30"
,
async
()
=>
{
const
fundraisers
=
await
factory
.
fundraisers
(
30
,
0
);
assert
.
equal
(
fundraisers
.
length
,
20
,
"results size should be 20"
);
});
})
In this code sample, our first test uses the normal it
that we have been using, but the following tests are marked with xit
to indicate they are pending. We can then remove the leading x
when it’s time to focus on that particular use case.
The size of the collection that is returned is now going to be the smaller of the fundraisersCount
or the limit being requested. If we just return a collection of the limit requested, our 0 size test will break. Example 7-7 shows our updated fundraisers
function.
function
fundraisers
(
uint256
limit
,
uint256
offset
)
public
view
returns
(
Fundraiser
[]
memory
coll
)
{
uint256
size
=
fundraisersCount
()
<
limit
?
fundraisersCount
()
:
limit
;
coll
=
new
Fundraiser
[](
size
);
return
coll
;
}
This will now pass our tests, but we’re not getting the right data. We are getting back a collection of the right size, but it will be an array of the 0x0
address. We’ll write more tests to assert against the contents in a moment, but for now, we will continue the focus on returning a collection of the appropriate size.
Moving on to the next test, we can remove the leading x
to let it run. When we do, we will see that it too is passing and that we have one more pending test to work through. The next test will fail because we haven’t declared a max size.
Our failure is shown in this output:
$
truffletest
Using network 'test'.
...omitted..
Contract: FundraiserFactory: fundraisers
when fundraisers collection is empty
✓ returns an empty collection (96ms)
varying limits
✓ returns 10 results when limit requested is 10
✓ returns 20 results when limit requested is 20
1) returns 20 results when limit requested is 30
...omitted...
Contract: Fundraiser
...omitted...
25 passing (10s)
1 failing
1) Contract: FundraiserFactory: fundraisers
varying limits
returns 20 results when limit requested is 30:
results size should be 20
+ expected - actual
-30
+20
To get this test to pass, we need to create our max limit. We will set this as a constant state variable on our contract and then update our fundraisers
function to use the minimum value of our fundraisersCount
, limit
, or maxLimit
. Our updated function is shown in Example 7-8.
contract
FundraiserFactory
{
// most items that can be returned from fundraisers function
uint256
constant
maxLimit
=
20
;
...
omitted
...
function
fundraisers
(
uint256
limit
,
uint256
offset
)
public
view
returns
(
Fundraiser
[]
memory
coll
)
{
// size should be the smaller of the count or the limit
uint256
size
=
fundraisersCount
()
<
limit
?
fundraisersCount
()
:
limit
;
// size should not exceed the maxLimit
size
=
size
<
maxLimit
?
size
:
maxLimit
;
coll
=
new
Fundraiser
[](
size
);
return
coll
;
}
}
With this change in place, our collection size seems to be working properly. We’ll now look into varying the offset.
The offset determines what index we use as a starting point. It will be in these examples that we also assert that we are getting the fundraisers that we expected. For these examples, we will keep the limit to just one and make sure that the beneficiary name includes the appropriate index. Example 7-9 illustrates our next test group.
describe
(
"varying offset"
,
()
=>
{
let
factory
;
beforeEach
(
async
()
=>
{
factory
=
await
createFundraiserFactory
(
10
,
accounts
);
});
it
(
"contains the fundraiser with the appropriate offset"
,
async
()
=>
{
const
fundraisers
=
await
factory
.
fundraisers
(
1
,
0
);
const
fundraiser
=
await
FundraiserContract
.
at
(
fundraisers
[
0
]);
const
name
=
await
fundraiser
.
name
();
assert
.
ok
(
await
name
.
includes
(
0
),
`
${
name
}
did not include the offset`
);
});
xit
(
"contains the fundraiser with the appropriate offset"
,
async
()
=>
{
const
fundraisers
=
await
factory
.
fundraisers
(
1
,
7
);
const
fundraiser
=
await
FundraiserContract
.
at
(
fundraisers
[
0
]);
const
name
=
await
fundraiser
.
name
();
assert
.
ok
(
await
name
.
includes
(
7
),
`
${
name
}
did not include the offset`
);
});
});
In addition to the tests in Example 7-9, you will need to add the following line to the top of the test file to load the Fundraiser:
const
FundraiserContract
=
artifacts
.
require
(
"Fundraiser"
);
In our new tests, we create 10 fundraisers through the FundraiserFactory and test against setting the offset to 0 and then setting the offset to 7. These numbers were arbitrarily selected; to be more complete, we could create a test helper setting the offset to all the values from 0 to 10 and assert that the offset is being adjusted appropriately.
Running the test will get a failure like this:
$
truffletest
...omitted...
Contract: FundraiserFactory: fundraisers
...omitted..
varying offset
1) contains the fundraiser with the appropriate offset
...omitted...
26 passing (11s)
1 pending
1 failing
1) Contract: FundraiserFactory: fundraisers
varying offset
contains the fundraiser with the appropriate offset:
Error: Cannot create instance of Fundraiser; no code at address 0x000000...
Now is when that 0x0
address mentioned previously is preventing us from passing our test. Now it’s time to fill the collection we are returning with actual fundraiser addresses. We can update the body of our fundraisers
function to look like Example 7-10.
function
fundraisers
(
uint256
limit
,
uint256
offset
)
public
view
returns
(
Fundraiser
[]
memory
coll
)
{
uint256
size
=
fundraisersCount
()
<
limit
?
fundraisersCount
()
:
limit
;
size
=
size
<
maxLimit
?
size
:
maxLimit
;
coll
=
new
Fundraiser
[](
size
);
for
(
uint256
i
=
0
;
i
<
size
;
i
++
)
{
coll
[
i
]
=
_fundraisers
[
i
];
}
return
coll
;
}
That is now letting our first test pass. Let’s move on to the second test. Remove the leading x
and then run the tests again. Now our test fails, with a message similar to the following:
1) Contract: FundraiserFactory: fundraisers
varying offset
contains the fundraiser with the appropriate offset:
AssertionError: Beneficiary 0 did not include the offset: expected
false to be truthy
We need to adjust our starting point when filling the collection with Fundraisers. Adjust the for
loop in our fundraisers
function to include the offset, and we’ll get back to green tests.
for
(
uint256
i
=
0
;
i
<
size
;
i
++
)
{
coll
[
i
]
=
_fundraisers
[
offset
+
i
];
}
There are just two more cases that we should handle. In the case where the offset is larger than our fundraisersCount
, our application will need to exit with an out-of-bounds message. We should also watch out for cases in which the sum of the offset and limit would exceed the size of our fundraisersCount
.
Let’s create another test group with our boundary cases and add the tests in Example 7-11.
describe
(
"boundary conditions"
,
()
=>
{
let
factory
;
beforeEach
(
async
()
=>
{
factory
=
await
createFundraiserFactory
(
10
,
accounts
);
});
it
(
"raises out of bounds error"
,
async
()
=>
{
try
{
await
factory
.
fundraisers
(
1
,
11
);
assert
.
fail
(
"error was not raised"
)
}
catch
(
err
)
{
const
expected
=
"offset out of bounds"
;
assert
.
ok
(
err
.
message
.
includes
(
expected
),
`
${
err
.
message
}
`
);
}
});
xit
(
"adjusts return size to prevent out of bounds error"
,
async
()
=>
{
try
{
const
fundraisers
=
await
factory
.
fundraisers
(
10
,
5
);
assert
.
equal
(
fundraisers
.
length
,
5
,
"collection adjusted"
);
}
catch
(
err
)
{
assert
.
fail
(
"limit and offset exceeded bounds"
);
}
});
});
In our new example group, we first test the situation where our offset is outside of our fundraiserCount
. Our expectation here is that the error thrown will include the out-of-bounds message.
Running our tests, we will have a failure that indicates an invalid opcode, like this error:
1) Contract: FundraiserFactory: fundraisers
boundary conditions
raises out of bounds error:
AssertionError: Returned error: VM Exception while processing transaction:
invalid opcode: expected false to be truthy
at Context.it (test/fundraiser_factory_test.js:161:16)
This is happening when we are trying to access an index that doesn’t exit in our for
loop. We can add a require
statement to give this error more meaning. Adding the following line of code to the top of our fundraisers
function will be enough for our test to pass:
require
(
offset
<=
fundraisersCount
(),
"offset out of bounds"
);
Remove the leading x
from our pending test, and running the test, we get a failure like this one:
1) Contract: FundraiserFactory: fundraisers
boundary conditions
adjusts return size to prevent out of bounds error:
AssertionError: limit and offset exceeded bouds
at Context.it (test/fundraiser_factory_test.js:174:16)
In this case, the offset meets the criteria of our require
statement, but the iteration in the for
loop causes us to access places in memory beyond our fundraisersCount
. We will update our fundraisers
function to start the local size
variable to represent the difference between the fundraisersCount
and the offset. The rest of the logic will keep us from going too far. Example 7-12 has the final version of our fundraisers
function.
function
fundraisers
(
uint256
limit
,
uint256
offset
)
public
view
returns
(
Fundraiser
[]
memory
coll
)
{
require
(
offset
<=
fundraisersCount
(),
"offset out of bounds"
);
uint256
size
=
fundraisersCount
()
-
offset
;
size
=
size
<
limit
?
size
:
limit
;
size
=
size
<
maxLimit
?
size
:
maxLimit
;
coll
=
new
Fundraiser
[](
size
);
for
(
uint256
i
=
0
;
i
<
size
;
i
++
)
{
coll
[
i
]
=
_fundraisers
[
offset
+
i
];
}
return
coll
;
}
Whew! That wraps up the Solidity code we’ll need to support our application. The next section will walk through setting up the UI so that you can click around the application.
Like our Greeter contract, we are going to download the UI and deploy locally to Ganache so that we can interact with our application via browser instead of only through the tests.
If you haven’t already downloaded the book code, you can access it from GitHub. Assuming the cloned repository is a sibling of our fundraiser directory, you can use the following command to copy the files:
$
cp -r ../hoscdev/chapter-10+11/ ./client
In a new terminal tab, enter the client directory and install the packages using the following commands. We’ll reference this terminal as the client terminal.
$
cd
client$
npm install
Once all the packages have been installed, we can update our truffle-config.js file with host and network ID keys for the develop
network. The final configuration will look like Example 7-13.
const
path
=
require
(
"path"
);
module
.
exports
=
{
// See <http://truffleframework.com/docs/advanced/configuration>
// to customize your Truffle configuration!
contracts_build_directory
:
path
.
join
(
__dirname
,
"client/src/contracts"
),
networks
:
{
develop
:
{
host
:
"127.0.0.1"
,
port
:
8545
,
network_id
:
"*"
,
}
}
};
Fire up the Ganache app and create a new workspace. When prompted for a project, include the truffle-config.js file and save the workspace. With that running, we are ready to migrate our contracts.
In the terminal located at the root directory of our fundraiser application, use the following command:
$
truffle migrate --network develop
In our browser, import the new accounts created by the workspace to MetaMask by using the mnemonic in Ganache.
When the migration completes, we’re ready to start the client server. Back in the client terminal, run the following command:
$
npm run start
The command will open a browser tab for you, taking you to your app. Congratulations! You can now create fundraisers and make donations on your local blockchain.
In this chapter, we created a contract mostly to handle the creation and the management of a potentially large collection of fundraisers. In doing this, we learned that contracts can initialize other contracts and we also dove into pagination.
We spent a lot of time testing our pagination cases to make sure we got it right. This led us to covering empty cases and various limits (some of which exceeded our maxLimit
) and also moving the offset around, including to places outside the range of the collection. With these features wrapped up, it’s time to start learning more about Web3 and the frontend technologies that we can use to build the UIs of our applications.