5 Followers
·
Follow

A Simple Solana Dapp Tutorial

This tutorial explains step-by-step how to write, deploy, and interact with a smart contract on the Solana Blockchain. When I was learning Ethereum, the first tutorial I looked at, was for a simple voting program. I’ve taken it and translated it into Solana, aiming at exact functional compatibility. I wanted to see the similarities and differences between Ethereum and Solana from a developer’s perspective.

At the end of this tutorial we will have deployed a voting contract on Solana mainnet, with a website frontend, it will look like this.

Image for post
Image for post
Topical, right?

Check out the completed Solana Dapp Tutorial and the code.

This tutorial does require decent shell and programming knowledge. Solana is a very new blockchain and is being rapidly developed, the tutorial was written 30th Aug 2020 and has been tested on Ubuntu 18.04, invariably thing will change/break. Find me on the Solana Discord.

Setting up the development environment

Solana on-chain programs (contracts) can be written in C, C++ and Rust, although the expectation is that most people will use Rust, and this example is written in Rust.

Initially we will get the program running in a local Solana node, then we will deploy to devnet, testnet and finally mainnet.

The off-chain programs we write will interface with the Solana node using a JavaScript API, this can be used for stand alone scripts executing on nodejs, or in webpages.

Let’s start by checking we have all the bits we need:

Please follow these instructions for installing Rust and Docker. Node has to be version 10.0.0 or higher, these installation instructions may help you, I installed v12.18.3 of node.

Clone the github:

Install the required modules, the modules and versions are defined in package.json, in particular the version of the Solana JavaScript web3 API we will be using, defined by “@solana/web3.js”:

Get the docker image, containing the Solana node. This is our ‘local blockchain’ similar to Ganache on Ethereum. The particular image it grabs is defined in package.json “testnetDefaultChannel”, in this case v1.3.9:

Start it (you can stop it later with; . stop_docker):

If you want you can tail the log:

The node listens on port 8899, the docker machine forwards to the node, you should see the machine if you do this “ps auxww | grep 8899”. Check you can talk to the node.

If you have the Solana client installed, you can run commands against the node directly:

Or you can do it via a script, I have done almost everything with scripts, so you can see how to do things programatically:

This script will return some details about the “cluster”, which in this case is just our local node. If you run the script again, the slot number will have increased, you can think of a slot like a block in Bitcoin or Ethereum.

By the way, the scripts we are running are defined in package.json, if you want to look at them they are plain JavaScript in the ./src/client/ directory. The script executed by “npm run slot” is ./src/client/slot.js

We need a wallet, which is just a public-private keypair:

There will now be a keypair.json file in the project directory, the file contains a sequence of bytes as decimals, the first 32 are the secret key, the last 32 the public key. For convenience, when being used and communicated, the public key is encoded as a base58 string, you can consider this the ‘address’ of the wallet. Creating a keypair is an off-chain operation. As the file is the only record of the secret key, if you delete it, that wallet is gone and unrecoverable.

We are using my scripts in this tutorial so you can see in detail how things work, if you are creating a wallet on mainnet to hold non-trivial amounts, please use the official Solana programs.

If you look at the script, you will see that we didn’t chose a private key, we just called the JavaScript web3 Account api, which used entropy from the operating system to create a random private key.

Let’s check the balance on our new wallet:

This will show your account public key (like an Ethereum address) and a balance of zero. We need some funds on the wallet, so that we can deploy our smart contract and do other on-chain stuff.

This will drop 10 Sol onto the account, airdrop is only available on our local node and devnet.

Actually our account didn’t exist until just now, the action of sending funds to an address is what calls the account into existence. In Solana, accounts are charged a very tiny amount of ‘rent’ every epoch, when the balance of an account goes to zero, it returns to the void... There is an exception to this rule, above a certain balance, an account is rent exempt and lives forever. More on this later.

If you want to see a bit more about your account, or any account, I’ve written a little script to dump out the contents, try it:

You see here: the balance; that the account is not executable; that it is ‘owned’ by the System Program (long string of 1s); and there is no data within it. All accounts are ‘owned’, obviously we control this account, but it’s owned by the System Program.

Writing and Testing a Really Simple Smart Contract

Now that we are all set up, let’s make a smart contract. First we will construct the simplest possible working version, then revisit and extend it.

If you are looking for a contract program to clone to get started, don’t use this one, use the next one. This version is intentionally over simplistic, to make certain points.

The way programs work in Solana is a little different from a Solidity contract on Ethereum, where code and data ‘live’ at the same place (at least conceptually). In Solana, the compiled program code is loaded into a special account, which can never be changed. If the program needs any storage on the chain, this must be held in a separate account(s), owned by the program. These accounts are then passed into the program in the transaction call, allowing the program to read and write the storage.

Our program allows only two actions: vote for candidate A or B, and it does only one thing: keep count of the votes!

You can find the complete code in /src/simplest-rust/src/lib.rs and you can build it using:

The Solana virtual machine is a Berkeley Packet Filter VM, various output of the build can be found in the /src/simplest-rust/target/bpfel-unknown-unknown/release directory. The all important file simplest.so is a eBPF (magic 0xf7) shared object in /dist/program/, this is the contract program we will deploy to the chain.

The program is very simple, before we deploy it, let’s go through the code looking at the important bits. Hopefully you know some Rust, because before making this tutorial, I didn’t know Rust and it is a little different to Solidity 😉

Here is the entry point of the program, the node will invoke the program with these parameters:

The invocation occurs when we make a transaction, therefore the transaction includes this information. The program_id is the public key (32 bytes) of the account the program is in, this is required because the same program could be deployed to different accounts, and it may need to know which instance is executing. Next an array of account structures are passed, each contains the public key of an account and various information about the account. Finally an array of bytes is passed, this is how we pass arbitrary arguments and data to the program.

For the super simple version of our voting program, users will pass the account to hold the vote count of the two candidates, and a single byte of instruction data to indicate which candidate they are voting for.

The accounts structure looks like this:

In our program the following lines extract the first account passed:

We then perform a check that the account is owned by the program, this is necessary for the account to be modified by the program. You might be wondering where this account is… it doesn’t exist yet! We will create the account after we deploy the program, and it will be created as owned by the program.

Accounts can be thought of as bits of on-chain storage, this account needs to hold two 32bit unsigned integers, representing the vote counts of the candidates. The first 4 bytes will be the vote count of candidate 1, the next 4 bytes will be the vote count of candidate 2.

Now the important bit, we read the account data from the chain, change it as appropriate, and write it back.

In the next contract program, we will use a nicer way to pack and unpack from the blockchain. For now, just realise that nothing magical is going on, we are just reading some bytes from the blockchain, interpreting them in a certain way, changing them, then writing the bytes back to the blockchain.

If you haven’t already, at this point build the program, make sure everything compiles and the solana_bpf_simplest.so file is generated.

We will test the program on-chain later, but at this point we can write some off-chain tests in Rust to make sure everything is as expected. You will find the test module in the same file, directly beneath our program, the comments explain what is happening. Execute the tests like this:

If for some reason you want to want to start again, you can do so like this:

It’s time to deploy the program and its associated data account on our local node. Check the node is still running and that our wallet has tokens on it. If necessary, start the node and fund your account as previously described:

Deploy the program:

Hopefully the script executed without error and you have a bunch of output, including two public keys (the program account and the data account), you can think of the public keys like addresses. These new accounts (and therefore their public keys) were generated just like our own account; off-chain, from entropy, i.e. randomly.

There is quite a lot going on here, so we should break it down. It may help to also look at the script we just executed.

From the output on your screen, find the public key of the program, then look at the account like this:

We see the program account is executable, it is owned by the BPFLoader, it has a balance, and it has a ton of data; the program bytecode.

All program accounts are created by the BPFLoader, which is also the owner, the data in the account are the bytes read from the binary .so file, program accounts are created once and can never be changed.

You might be surprised that the program account has a balance. The reason is that Solana charges ‘rent’ on accounts, periodically (every epoch) a little bit of the balance is taken, at zero the account is deleted. There is an exception, above a certain balance, an account is rent exempt. The BPFLoader creates the program account with the required rent exemption balance for the size of the program.

In case you want to programatically compute the cost of a load before invoking BPFLoader, in deploy.js I made a function estCostLoadProgram which you can look at.

After loading the program, we created the data account. We created a transaction to the System Program to make this account on-chain sized at 8 bytes (at the time of writing accounts are fixed size forever, but this may change in the future) and transfer the required rent exemption to it (which we had to calculate) and the owner of the new account was to be the program. You can check the program ownership using dump, you will also notice the 8 bytes (zeros) of account data.

NB: The deploy script also keeps a record of the program account public key and the program data account public key in store/simplest.json, this is not essential to what we are doing, it’s just convenience. If you want to start over, doing a clean_simplest will remove this config.

Small thing to mention; new data accounts always have all their data initialised to zero, for us that is a happy coincidence as our vote counts should also start at zero, but what if you needed the new account initialised differently? As the program must be the owner of the account, you would need to initialise through the program using instruction_data. Not only that, you would need the program to require the new account was a signer on the transaction, to prevent it being spoiled by a bad actor.

Let’s call the program to see if it works. Vote for the second candidate like this:

Our vote cost 0.000005 Sol, you can vote as often as you like, so you should.

The vote_simplest script creates and sends a transaction to call the program with the candidate to vote for, the program updates the blockchain, then reads back the data and displays it.

You can also use dump on the data account to see the changes to the data. I voted for 2 twice and 1 once:

That’s the end of this section, we developed a very simple contract program, deployed it to our local node, called it, and read the data from the blockchain.

This version of the voting contract is called “simplest” for a reason, it’s far too simple. For example there is nothing to stop the same person voting multiple times. We will come back to the voting contract and make changes to prevent multiple voting. But first let’s talk about debugging and how to deploy to “real’’ networks.

Debugging and Troubleshooting

If you need to debug, dump the local node’s log to a file (see previous) and search for the program account address. Beware, that (for reasons I don’t understand) the docker node’s time is just totally wrong, it’s not UTC, it’s not local time, it’s not any timezone. When you find the BPF program execution, you’ll find your error, just for example:

If you need to stop and restart the docker image: . stop_docker works on my Mac, but on Ubuntu I had to list the containers “docker container ls” then “docker container stop <theSolanaContainer>”. The start_docker script seems to work for both.

In terms of configuration, version control and changes, there are a few moving parts here. The Solana Rust SDK is being developed, the JavaScript SDK is being developed, The BPF SDK is being developed, the Solana node on the docker image will be changing. At any given time, you need these components to be compatible. Additionally, Rust itself is relatively new and is being developed. With so many moving parts, It is very easy for things to break. If things don’t work the following may help:

  • To get a BPF SDK version use: npm run bpf-sdk:update (remember to rebuild afterward), this component is what translates your rust code into BPF byte code, the version it gets is driven by the package.json field “testnetDefaultChannel”. Any problems building rust, you might want to try this.
  • To get a docker image version use: . update_docker (remember to stop & restart afterward), the version it gets is driven by package.json field “testnetDefaultChannel”.
  • The version of the Solana Rust SDK crate “solana_sdk” being used is controlled by Cargo (Rust’s crate manager) look at Cargo.toml. This is the Solana specific code our program uses, like AccountInfo, etc. NB: if the version is specified like this: version = “1.3.14” it uses that version or newer, but version = “=1.3.14” means use that exact version.
  • The version of npm modules, including the JS SDK is driven by package-lock.json and package.json look for solana/web3.js under dependencies. NB: if a version number is preceded with a ^ character, it means that version or greater, without the ^ character, it means that exact version.
  • When changing the Rust SDK or JS SDK, your program / JavaScript code may need to change.
  • To get the latest rust build use: rustup toolchain install nightly.
  • To try to determine the ‘break’ you can also download a reference implementation supported by the team, such as “hello world”, then play spot the difference. The “hello world” implementation is guaranteed to always work, if it doesn’t let them know in the discord.

12th Oct 2020, the following works for local node:
- testnetDefaultChannel: v1.3.15
- solana_sdk: 1.3.14
- solana/web3.js: 0.78.2
- rustc version: 1.45.2
- use
BPF_LOADER_DEPRECATED_PROGRAM_ID in client/deploy.js
- use
entrypoint_deprecated in contract code to match the above
^ Probably, by the time you read this, deprecated will no longer be used, in both of the files I have the lines next to each other with one commented, so it is simple to switch

Deploying on devnet, testnet, mainnet

Solana has three networks: devnet, testnet and mainnet. See the documentation for latest information. The connection details (the node IP and RPC Port) are in nodeConnection.js but these may change over time.

Devnet is currently just a single node run by the Solana team. This is not really much different from running the local docker node; it does save you running your own, and it is accessible by everyone so you can develop with others. It has airdrops enabled for tokens.

Testnet (“TDS”) is an independent test network run by 100s of nodes globally. Ask in the discord for tokens if you want to deploy to it, someone will give you. This network often runs a newer code branch to mainnet, unless you are developing ahead of mainnet functionality, you may want to skip this network.

Mainnet is the actual network run by 100s of nodes globally. Don’t be misled by the “mainnet-beta” label, this is real. Tokens have value, you can buy Sol on Binance.

devnet

To deploy to devnet, switch to that network and check it is running:

The cluster_devnet script copies the relevant file from the /env directory, which is read in the nodeConnection.js script, causing the url of devnet to be set. The script also removes all store config, so if you switch back to cluster_local, you will need to redeploy the contract.

Previously your account had a balance, but that was on your local node, on devnet it doesn’t. Airdrop to your account:

Deploy the contract and it’s data account:

Vote for a candidate:

That’s pretty much it, but because the devnet node probably isn’t on the same code point that your local node was on, you may need to change some of your configuration. At you can see above, the devnet node is on 1.3.16 whereas our local node and the BPF SDK were on release 1.3.15, nevertheless, everything worked without changes. If you have problems, see the “Debugging and Troubleshooting” section previously.

testnet

Same thing… testnet.solana.com:8899 is currently servicing requests, if that will continue I don’t know. There are no pay-to-RPC services on Solana like Infura on Ethereum, the team just run that node with the RPC port open, you could also run your own node and use that.

Typically devnet and mainnet are the same, but testnet maybe on a different branch and require different config in terms of versions. I usually go straight from devnet to mainnet.

mainnet

Same story for mainnet, right now the following works:

If that’s no longer available, you can run your own node or perhaps convince someone to open their RPC port for you. Although on mainnet people will probably not want to do that, as it may degrade their node’s performance which has financial implications.

And that’s it, we’ve deployed to mainnet. Next we will make changes so that the same account cannot vote more than once.

Keeping a Record of Who has Voted

This section is about extending the contract to prevent duplicate voting, demonstrating pack and unpack, custom errors. if you are not interested in this, move ahead to the next section where we create a web interface to the voting system. I’ll be using the docker local node for this section, but really you can develop against any network as you wish.

So far our Solana Rust contract looks pretty similar to an Ethereum Solidity contract, but you will have already noticed that in Solana you control things “at a lower level”, much less is taken care of for you. For example, the contract program and the fixed size contract storage were created in separate steps; you wouldn’t do that in Solidity, the contract storage is automatically and dynamically allocated.

If we wanted to prevent double voting in Solidity, we’d do something like this:

Pretty simple, we just create a “mapping” up front, then set the calling account’s entry to true (default entry on map creation is false) when they vote, we require the calling account’s entry is false. I don’t know how the Solidity compiler is taking care of that under the covers, but for the developer it’s very straightforward.

In Solana we could, if we knew the number of voters, create a fixed size array (or something) in the contract’s data account, which we could record votes in, but this would almost certainly be the wrong approach. What we need is an expanding mapping similar to what Solidity has. To do this in Solana we need to create a new account for every voter. The account needs to be created by the client and passed into the contract. This VoterCheckAccount must be somehow related to the voter’s account, so that the relationship is 1:1 and deterministic. The contract will use that same deterministic logic to locate the VoterCheckAccount and read/write to it. It’s not simple.

This is what needs to happen when the user votes:

  1. Client creates VoterCheckAccount at an address, derived deterministically from user’s account, to store an integer.
  2. Client makes contract call, passing in VoterCheckAccount.
  3. Contract checks address of VoterCheckAccount passed in is correct as per voter’s account.
  4. Contract requires the VoterCheckAccount contains 0
  5. Usual voting logic happens.
  6. Contract writes integer 1 or 2 (candidate number) into the VoterCheckAccount.

And that’s the simplified version, more things will have to be done to make the contract secure, as you will see later.

Note that the same address derivation needs to happen in the client (JavaScript) and in the contract (Rust).

To keep things easy for this tutorial, I have created a separate rust program and separate scripts. The npm scripts are similar, but instead of the _simplest postfix, they have a _rejectdups postfix. And there is a rust directory called rejectdups-rust, that way we can preserve both versions of code and see what changed.

Client Changes

In the client we can use this function from the web3.js library:

This will gets us a public key (address) in a deterministic way, we then use:

Which will create a transaction which will create an account, owned by the program, at that location. Neither we, nor anyone else, will ever know the secret key for that account.

To demonstrate the concept programatically, I have made a script demo_caws.js that creates a new account based on our account, a seed ‘somestring’, and the programId.

Ensure the program has been deployed (npm run deploy_simplest), then run the demo like this:

From the output take the new account address (the pubkey), inspect the account like this:

You will see the account is owned by the program, and has 4 bytes of zero initialised storage.

If you run the demo again, it will fail, because the account already exists. If you do a clean, and deploy the program again to a new address, then you can run the demo and it will work, but only once. For any account, seed and program — there is exactly one corresponding address.

The base public key must be the public key of the transaction signer, otherwise anyone could ‘steal’ the account, the node enforces this.

Contract Changes

Unlike in our previous simple example, we will be implementing traits program_pack::{Pack, Sealed} for reading and writing data. Here is the implementation for the instruction data. We have a Vote struct that contains an 8 bit unsigned integer. The implementation requires a unpack_from_slice and pack_from_slice function, these create a Vote from data and create data from a Vote respectively. We don’t define pack_from_slice as we will only be reading the instruction data. The implementation also requires the data be exactly 1 bytes long. And that the integer which represents the candidate to vote for be 1 or 2 only.

We use the above to get the candidate our user is voting for:

The first account (remains) the contract’s data account, which holds the total vote count. It must be owned by the program:

The second account will be the VoterCheckAccount. It must be owned by the program:

We need to ascertain if the VoterCheckAccount will live forever, i.e. it must have been created with a sufficient balance to be rent-exempt. If the account has less than the rent exemption, its balance will be gradually depleted due to rent collection, until it hits zero and is deleted. At that point the voter could create the account again, and vote a second time!

To do the rent calculation, we need to pass in a special system account, the third account is that account. We verify that it is the system rent account, and that the check-account is rent exempt:

The forth account is the voter’s account (VoterAccount), which must be a signer of the transaction, proving they are making the call:

We now check that the VoteCheckAccount passed in, is indeed that belonging to the VoterAccount, and not some other account:

We get a pointer to the VoterCheckAccount data:

Like the instruction data we will use Pack to deserialise the data:

Used here:

We then check that the voter has not already voted:

We now need to read, increment and write the candidate count data. Again we implement Pack to deserialise/serialise the data. I won’t show the implementation here, look at the code if you want to see it. As you can see the last two statements are the writing of the new counts and the candidate voted for to the VoterCheckAccount.

One last thing, you may have noticed that some of the errors the contract program returns look a little different, for example:

The provided system errors don’t always exactly fit, so I’ve made some custom errors, you can see them defined near the top of the contract program:

That really is everything, so we can now build the code as follows:

Note: I got a build error here, for reasons unclear. If you do too, try: cd /src/rejectdups-rust; cargo update; npm run bpf-sdk:update

Having built the code, the dist/program/ directory will contain rejectdups.so deployment is basically identical to how it was with the previous program:

And vote:

Although this looks exactly the same as vote_simplest, the script does a bit more, it creates the voter’s check-account and passes it into the call, as well as passing the special system rent account and the voter account into the call. You probably noticed that voting just got a little more expensive. Check out the script for details.

Try voting again, the client script checks the voter’s check-account and can see they already voted… but even if it didn’t, the on-chain program would reject the vote:

If you dump out the voter’s check account, you can see who the candidate voted for, it’s stored forever on the blockchain:

What we basically did here is implement a mapping. I never really thought about how Solidity worked under the covers, and I honestly still don’t know, but now that I have worked though a low level implementation on Solana, I do kind of wonder if it’s similar?

Let’s recap what we did to make a mapping:

  1. For each voter, create a new account at a ‘fixed offset’ from the base account using a seed.
  2. In the contract, a) check that this new account is rent exempt using the system rent account and b) that the system rent account is the system rent account.
  3. In the contract a) check that the new account is at the ‘fixed offset’ from the base account per the seed and b) the base account is a signer on the transaction

If any of those in-contract checks are missing, the contract is insecure.

We could keep enhancing the contract in various ways, but let’s stop now and make a web frontend.

Making a Web Frontend

So far we have been interacting with our contract via scripts, it’s time to create a web frontend, so that people from all over the world can vote. I’ll do all the following on mainnet, if you want to follow along in another environment, it should be should be straightforward.

We will need a wallet on the browser, so that users can transfer the voting fee to it and create the vote transaction. But first let’s create a super simple example of a webpage interacting with Solana.

You will notice all my examples use a local file:

This is a local copy of the web3 sdk that will be used on our webpages. The way I got this file (0.71.9) is like this:

There will be a nicer way of getting this in the future, but right now that works; but of course you can just use mine.

Let’s use a simple local web server initially:

Very Simple Browser Example

I’ve made a directory called frontend-simplest/ containing one file index.html which contains this code:

In that directory run http-server, then in an internet browser go to http://127.0.0.1:8080

You should see the transaction count of mainnet updating every second. This code is running in the user’s browser, making a direct connection to the RPC node.

A Simple Wallet

Let’s make a wallet that a user can transfer some coins to, so that they can cast their vote. There are various web wallets being developed for Solana, but to make things simple, we will just create an account and stick it in a cookie.

The code is in the frontend-wallet/index.html file, and can be seen below. The code below creates a wallet, displays the address and balance. Go to the directory and run http-server, then navigate to http://127.0.0.1:8080 in your browser. If you want, you can deposit a very small amount (0.01 Sol) to the address, it should show up within a few seconds (refresh the page). The “Show Secret Key” button shows you the contents of the cookie. The “Destroy Wallet” nukes the cookie and creates a new wallet (funds gone!).

Adding Voting Functionality

Now let’s add the ability to cast a vote.

At this point, we are going to have to change direction a little. Both the previous examples make a direct connection between the browser and the Solana node. There are two reasons why we won’t continue doing it that way.

  1. The part of the JS SDK we are going to use next calls sha256, which most browsers block if the connection is not https, there is a (deceptive) exception for localhost, but if we want to deploy as a real website we will need to run https. Easy enough, but browsers get funny when https sites have mixed content, our content is mixed because we are serving things coming from our server but also things from the Solana node, and the browser knows it and flags it as insecure.
  2. In the future it may not be possible to make anonymous calls directly to a Solana node, RPC calls cost compute cycles, it’s kind of unrealistic that it should be free to the public. If you use a pay-as-you-query service like Infura for Ethereum, you are given a secret token to make RPC calls, which you will be billed for, so you do those calls from your server. Anticipating this model, we will make the RPC calls from our server.

Any call we can make from the browser, we can also make from the server. Indeed if you wanted to, you could make some calls from the browser and others from the server. However, we will be making ALL calls from the server, the browser will still use the JS SDK for some things, but RPC calls will be ‘passed back’ to the server. We won’t be able to use our simple http server anymore. We will write a small server using node. The good news is that we already know how to make all the calls we need from the server; our npm scripts. We just need to make the functionality ‘callable’ from the browser.

NB: For this example, I started up the smallest possible cloud server on Digital Ocean — Ubuntu 18.04

The code is in frontend-vote/ there are two files: index.html and server.js. As in the previous examples, the index.html file runs in the browser, the server.js is our web server.

For this example you need to deploy the rejectdups contract to Solana mainnet, as previously demonstrated, plus we need the address of the contract and its data account. Here is my deployment with the important details in bold, it will cost about 0.9 Sol. Or if you don’t want your own instance, you can just use mine.

It will be easiest to start with server.js which uses some packages (if these don’t already exist on your system, install using npm), the Solana RPC endpoint to use is defined, then:

This function is called for each request made from a browser to our web server. But before the server starts listening, it does the bits after this function definition. Go to the end of the file and you will see the first thing it does is connect with the Solana node, it then creates the https server using key.pem and cert.pem files, after that it listens for requests.

For now make a self signed certificate like this:

The browser knows it’s a self-sign, there will be a warning, go to the page anyway (Chrome won’t let you, but FireFox will, so use that) — to actually deploy for real, we’ll need a proper certificate.

At this point you can start the server like this:

The URL will be whatever the IP of your server is, you may need to allow 443 traffic.

If you look at the code again, within the app.get function, there are various sections. The first one gets the balance for an address, the browser passes back an address in the URL, the server queries the Solana node for the balance and returns it. The important line is:

It’s identical to the npm scripts we used initially. The server deals with all the requests from the front end: it reads the vote counts, gets a recent blockhash (more later), reads the voter check-account, calculates vote costs and rent exemption. It also submits signed transactions, the transaction is made and signed in the browser (the secret key is never passed back to the server), the transaction is then turned into bytes and passed to the server as a JSON string. The server just sends it to the node:

And waits until the entire cluster has confirmed the transaction by 1 block:

The rest of server.js just serves up files, like index.html

We can now look at index.html to see what is running in the browser. You will notice the IP of the node server is defined, you must change that to be your server. The contract address and the vote account address are also defined as per our mainnet deployment. All the wallet functionality is there as in the previous example. There are a couple of small functions that fetch a URL from our server, one you wait for, the other takes a callback. There is a pageload() function that takes no parameters, and a vote(c) function that takes the candidate as the only parameter.

The pageload function gets the wallet balance, vote counts, voter status, etc, by doing a fetch from the server. The URL contains whatever information the server requires, for example, to get the balance of an account, the address is passed back. It’s very simple, and becomes clear just by reading the code.

The vote function is a little more complicated. It makes two transactions, the first to make the voter’s check-account, the second to vote by calling the program. You will notice the code looks very similar to the vote_rejectdups.js script. But there are some differences, because we are making and signing the transaction in the browser but broadcasting it from the backend, we cannot use the utility function as we did in the script, we have to do things at a lower level. Looking just at the first transaction, it is constituted in the normal way:

But then we get a ‘recent blockhash’ (via the server) and assign it to the transaction. This blockhash is valid for around 2 minutes, if the submitted transaction has not been mined by that time, it is guaranteed to be dead, forever.

We then sign the transaction. We then serialize the transaction into an array of bytes. We encode the bytes as a JSON string and pass this back to the server, which broadcasts the transaction to the Solana node. The second transaction happens exactly the same way.

And that’s pretty much it. There is one small refinement we can make, we can combine the two transactions into one. The index2.html file has everything the same, except it combines the check-account creation and the vote transaction:

Right after creating the transaction for the check-account, you can just add the TransactionInstruction for the vote, then assign the ‘recent blockhash’, sign and serialize as before. No other changes are required, you can just replace index.html with index2.html.

Some Lipstick on the Pig

As far as functionality goes, that is everything I am going to do. It’s now time to make a beautified version of the webpage and deploy it to the real internet. Take a look the finished Solana dapp tutorial hosted on my website.

All I’ve done is add a bunch of HTML, CSS and JavaScript — there is zero functional difference between this final version and the version we just made, but it does look nicer! If you want to check out the fancy formatting, it’s in the frontend-final/ directory.

I also had to get a proper certificate, which means giving it a subdomain and using certbot; didn’t cost anything, but did involve a few steps — outside the scope of this tutorial.

Contemplations

I set out to translate an existing tutorial from Ethereum to Solana, and in the process determine some of the similarities and differences.

What are the similarities? Ethereum has a ‘local blockchain’ Ganache, Solana has a node on a docker image. Ethereum has various testnets (Goerli, Ropsten, etc), Solana has two; a centralised (foundation maintained) devnet and a distributed (volunteer maintained) testnet. The web3 JS SDK for Ethereum and Solana are super similar.

What are the differences? The virtual machines are different, EVM vs BPF. The contract languages are different, Solidity vs Rust. But more than just different, Solana has to be programmed at a ‘lower level’, to construct a mapping is trivial in Solidity, but required quite a bit of work in Solana. On the plus side, Solana is much faster, from the UX perspective transactions are ‘mined’ almost instantly & because the capacity of Solana is huge (50Ktps), transactions are very cheap and will be for the foreseeable future.

I did everything to keep this tutorial as simple as possible. Now that you understand what is going on, you may want to look at a more sophisticated dapp (using React) check out break and the code.

If you are a hodler, please consider delegating some of your stash to our Solana validator, it helps keep the show on the road.

Written by

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store