Chapter 7. FundraiserFactory

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.

Migrating Our FundraiserFactory

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:

$ touch test/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:

$ truffle test
Using network 'test'.

Compiling your contracts...
> Compiling ./contracts/Fundraiser.sol
> Compiling ./contracts/FundraiserFactory.sol
  Contract: 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:

$ truffle test
Using network 'test'.

Compiling your contracts...
> Compiling ./contracts/Fundraiser.sol
> Compiling ./contracts/FundraiserFactory.sol

  Contract: 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.

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.

Example 7-1. Incrementing the fundraisersCount
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:

$ truffle test
Using network 'test'.
Compiling your contracts...
===========================
> Compiling ./contracts/Fundraiser.sol
> Compiling ./contracts/FundraiserFactory.sol

  Contract: 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.

Example 7-2. FundraiserContract counting the number of items in the collection
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:

$ truffle test

Using network 'test'.
Compiling your contracts...
> Compiling ./contracts/Fundraiser.sol
> Compiling ./contracts/FundraiserFactory.sol
  Contract: 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:

$ truffle test
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.

Example 7-3. Add FundraiserCreated event
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.

Viewing Available Fundraisers

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.

Testing Pagination When Empty

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.

Example 7-4. Setting up retrieval of our fundraisers
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:

$ truffle test
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.

Example 7-5. Defining the fundraisers function
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.

Testing the Limit

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.

Example 7-6. Testing the limit parameter
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.

Example 7-7. Determining the size of the collection to return
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:

$ truffle test
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.

Example 7-8. Limiting the size
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.

Testing 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.

Example 7-9. Testing offset
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:

$ truffle test
  ...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.

Example 7-10. Adding fundraisers to the collection
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.

Example 7-11. Boundary tests
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.

Example 7-12. Keeping in bounds
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.

Setting Up the UI

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.

Example 7-13. Truffle configuration
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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.172.115