Near Api JS with Rails 7

Avatar for wabinab
2 years ago

Introduction

One mentioned before that React isn't happy to developed with. Rails is better. Hence, we want to port some functionalities to Rails. Particularly, the near-api-js is what we need to call functions from corresponding smart contract(s) deployed separately.

You might ask, why not use Gem? Well, Gem requires separate development than keeping everything in one. If core team only updates the near-api-js, it would always have the latest functionality before the community updates the corresponding Gem. If you would like to look at gem, here it is. We will look at gem in another article, whenever one finishes experimentation on it; here, we focus on javascript.

Note before continuing:

Beware that one have very limited experience with Ruby and Ruby on Rails (barely 30 hours maybe?), so set your expectations. Things that professionals find a habit doesn't apply here.

Also, this is a technical blog!

If you decided to use bootstrap, please jump to the bootstrap section immediately before coming back to here.

Import near-api-js to Rails 7

Rails 7 introduces importmap. With importmap, you still can use npm, but not how you would do it like React does (npm i near-api-js). Instead, it integrates with importmap. After creating a new rails project, navigate into the project directory and call this function.

./bin/importmap pin near-api-js

If that does not work for some reason, force it by copying these into config/importmap.rb:

pin "near-api-js", to: "https://ga.jspm.io/npm:near-api-js@0.44.2/lib/browser-index.js"
pin "base-x", to: "https://ga.jspm.io/npm:base-x@3.0.9/src/index.js"
pin "bn.js", to: "https://ga.jspm.io/npm:bn.js@5.2.0/lib/bn.js"
pin "borsh", to: "https://ga.jspm.io/npm:borsh@0.6.0/lib/index.js"
pin "bs58", to: "https://ga.jspm.io/npm:bs58@4.0.1/index.js"
pin "buffer", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/buffer.js"
pin "capability", to: "https://ga.jspm.io/npm:capability@0.2.5/index.js"
pin "capability/es5", to: "https://ga.jspm.io/npm:capability@0.2.5/es5.js"
pin "crypto", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/crypto.js"
pin "depd", to: "https://ga.jspm.io/npm:depd@2.0.0/lib/browser/index.js"
pin "error-polyfill", to: "https://ga.jspm.io/npm:error-polyfill@0.1.3/index.js"
pin "http-errors", to: "https://ga.jspm.io/npm:http-errors@1.8.1/index.js"
pin "inherits", to: "https://ga.jspm.io/npm:inherits@2.0.4/inherits_browser.js"
pin "js-sha256", to: "https://ga.jspm.io/npm:js-sha256@0.9.0/src/sha256.js"
pin "mustache", to: "https://ga.jspm.io/npm:mustache@4.2.0/mustache.js"
pin "o3", to: "https://ga.jspm.io/npm:o3@1.0.3/index.js"
pin "process", to: "https://ga.jspm.io/npm:@jspm/core@2.0.0-beta.19/nodelibs/browser/process-production.js"
pin "safe-buffer", to: "https://ga.jspm.io/npm:safe-buffer@5.2.1/index.js"
pin "setprototypeof", to: "https://ga.jspm.io/npm:setprototypeof@1.2.0/index.js"
pin "statuses", to: "https://ga.jspm.io/npm:statuses@1.5.0/index.js"
pin "text-encoding-utf-8", to: "https://ga.jspm.io/npm:text-encoding-utf-8@1.0.2/lib/encoding.lib.js"
pin "toidentifier", to: "https://ga.jspm.io/npm:toidentifier@1.0.1/index.js"
pin "tweetnacl", to: "https://ga.jspm.io/npm:tweetnacl@1.0.3/nacl-fast.js"
pin "u3", to: "https://ga.jspm.io/npm:u3@0.1.1/index.js"

Add custom javascript to Rails

Next, we want to add a custom javascript to Rails. Follow the second answer from this link. Particularly:

(This is a copy of the answer)

  1. Go to config/importmap.rb and add the following: pin_all_from "app/javascript/custom", under: "custom"

  2. Go to app/javascript/application.js file and add the following: import "custom/main"

  3. In app/javascript directory, add custom folder.

  4. In app/javascript/custom directory add your custom js file main.js.

  5. Run In your terminal: rails assets:precompile

  6. Start your rails server. Voilà 👍

Add config.js

There are lots of React projects you can see online. Let's just take a look at the guest book example. In the config.js, we need to make some changes for it to work. In line 1, we need to manually define where our smart contract is deployed to: unlike react which saves the configuration in process, Rails cannot detect process keyword, as smart contract is deployed separately from it (React is deployed together). So for example:

const contract_name = 'guest-book.testnet';

Second, change function getConfig(env) to export default function getConfig(env).

Lastly, since we already export default, we can remove the last line (remove module.exports = getConfig).

Add contents from index.js to main.js

We named it main.js, but you could name it index.js too (just because the answer uses main.js one is lazy to change it). So in main.js (or index.js): Copy this content and put it in:

https://github.com/Wabinab/NCD_demo/blob/main/main_project/src/utils.js

We surely could call an init function when starting Rails Server. What we do is make changes at application.html.erb. In the <body> tag, add a javascript_tag before yield.

<!DOCTYPE html>
<html>
  ...

  <body>
    <% javascript_tag "javascript:initContract(#{Rails.env})" %>
    <%= yield %>
  </body>
</html>

So we added an extra input to pass in Rails.env to initContract, let's also update our function: (This replaces the process.env that Rails cannot detect we mentioned before):

export async function initContract(environment) {
  const nearConfig = getConfig(environment)
  const near = ...
  ...
  window.contract = ...
}

Remember to update the viewMethods and changeMethods with the function names deployed at your contract! Also, remember to update config.js accordingly to reflect the three environments from Rails (production, test, and development). (Actually, the original code one sees already have the three tags, so most probably you can ignore, just double check to make sure).

There's a caveat though: if you're at the home page (i.e. root_path), you cannot discover those window.something while on it. Only if you move to other paths will it be discovered. The error isn't something solvable unless removing it outside function.

Let's look at the login and logout function next.

What every function needs

For every function, exporting them to window is required, otherwise Rails cannot find them.

function login() {
  window.walletConnection.requestSignIn(nearConfig.contractName)
}

window.login = login

So for our original contract, we need to add extra two lines in main.js: At the end of the file, add:

window.login = login
window.logout = logout

If you have more functions, do that for all of them.

What works in ERb

Not all ERb function works. As far as one experimented, only link_to works. One only experimented with link_to and button_to, but there are others that are known to work like javascript_tag that one didn't experiment with; but you can try yourself. Oh, button_to don't work. If you want to use a button, you can do two ways:

  • Define the class on link_to. Example with bootstrap (we'll discuss bootstrap next and its issues)

<%= link_to "Login", "javascript:login()", class: "btn btn-primary" %>

(see the class tag, which is optional). Another would be to not use ERb, just use a normal HTML button will work:

<button onClick="javascript:login()">Sign in</button>

One didn't experiment with javascript_tag, so you shall experiment it yourself.

Setup Bootstrap

If you started off running rails new project-name --css=bootstrap for rails v7.0.2.3 (or before), you're doomed!

As of v7.0.2.3, bootstrap does not initialize with importmap, but with turbolinks. What we've done so far exists on the basis of importmaps. Turbolinks might work, but you need to ask someone specialized with turbolinks to ask the equivalent; or if you're professional with turbolinks, go forward translating the instructions yourself.

If you start off with rails new project-name, let's see how to migrate to bootstrap. Based on the cssbundling-rails github page, we run these commands:

./bin/bundle add cssbundling-rails
./bin/rails css:install:bootstrap

If you jump immediately to here as one mentioned, then the next paragraph aren't applicable: "build:css" will be created for you automatically. If you already add other stuffs, it fails unfortunately, so you need manual adding.

These aren't complete though. Try running yarn build:css and you'll encounter error, as package.json has missing build:css. This isn't the problem if you ran rails new project-name --css=bootstrap; so we can just copy the commands from that package.json and paste it here for it to work. For your convenience, one attached the command below for v7.0.2.3 (one do not promise other Rails versions uses the same command, do your own research (DYOR)): Add this under scripts: (which should just currently have a test)

"build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss ./app/assets/builds/application.css --no-source-map --load-path=node_modules"

Compilation of CSS also have errors. Fortunately, solving them is easy; just run these commands everytime you updated the application.bootstrap.scss file, including any custom scss and perhaps css files you've defined in the app/assets/stylesheets folder.

#!/bin/bash

yarn build:css
rails assets:precompile
bin/dev

yarn build:css might be optional. For those developing in AWS Cloud 9 (like one does), follow Michael Hartl tutorial to setup rails server properly (otherwise cannot preview). In fact, bin/dev does not work with the setup, so after the server run, press CTRL+C to exit, and run

...
bin/dev
# wait for it to boot up finish, the command line will stop updating if you don't 
# do anything to it. 
CTRL+C
rails server

As long as you don't update any stylesheets, you can retain rails server. Otherwise, run through this command from yarn build:css again.

What you can do and can't do

We can get value like this: (theoretically)

<% @varname = "javascript:window.walletConnection.isSignedIn" %>
<% link_to "Create Post", @varname %>

But practically, one couldn't test the above code, because the returned value is a boolean, not a link to some page.However, if you have a function that returns a hyperlink, it certainly works, so we would assume it works. This works:

<% @param = "javascript:getParameters()" %>

where in Javascript:

function getParameters() {
  return "message = hello_world";
}
window.getParameters = getParameters

But this doesn't work:

"javascript:window.getParameters()"

fails with

Uncaught TypeError: window.getParameters is not a function

When one tries to call the contract like this:

<% @param = "javascript:window.contract.get_article_by_id({'article_id': 'some_id'})" %>

It works! The result are in F12 browser console though, so open that up first before calling the function to see the results.

And you certainly could pass in arguments just like how you usually append something to a string in Ruby:

"Some variable in Ruby: #{variable_name}"

This is useful if you have to take some input from users, or inputs from users' actions that lead to some choices to insert to the string.

Turn it into a function

window.contract.get_article_by_id is not fancy to use within ERb, so let's move it to Javascript. We want to reduce our action to something like this:

"javascript:get_article_by_id('#{variable_name}')"

(Note the single quote around #{...} if you want to make it a string; if you forget these, it will fail if it should be a string input; if float or integer, it might pass, one isn't sure).

Inside main.js, add this:

function get_article_by_id(article_id) {
  return window.contract.get_article_by_id({
    "article_id": article_id
  }).then(
    value => alert(value),
    err => alert(err),
  );
}

window.get_article_by_id = get_article_by_id

So this function returns a promise, and the .then() will give a popup with the return values once the Promise is fulfilled or error. If fulfilled, it'll do whatever you ask in the value strand, while if it failed, it'll do whatever you tell in the err strand. Here, both are alert, so that makes no difference. Try to use console.log for one and alert for another and you could see the differences.

We could repeat these for other contract functions also, depending on how you want to handle it.

Conclusion

Adapter to near-api-js works! It isn't as convenient as gem functions, though. Following Rails convention over configuration rule, you might prefer near api with Gems (which one can tell you, isn't mature yet). Until it becomes mature, resorting to adapter with Javascript is okay; we can migrate to gems later on when things got more mature.

Additionally, developers also require programming in Javascript, which we try to reduce to as little as possible when using Rails. Though Rails support javascript, having to run functions wrapped in string quotes aren't convenient; and there may be lots of integration problem with fetching data from javascript too! Imagine writing getter functions just to get functions that doesn't work by calling window.something directly. It's gonna flood our javascript.

One might continue on with building an application with Rails and NEAR Protocol so stay updated with future blogs. Like and Subscribe!

References

1
$ 0.06
$ 0.06 from @DrPsycho
Sponsors of wabinab
empty
empty
empty
Avatar for wabinab
2 years ago
Enjoyed this article?  Earn Bitcoin Cash by sharing it! Explain
...and you will also help the author collect more tips.

Comments

Yeah I see that it's a technical blog. That's why it went over me. I am hardly capable understanding basic java😂( and basic isn't what you think basic is, it's much more basic than that).

$ 0.00
2 years ago

Hahah yeah it's a difficult decision: technical audiences and non-technical audiences... perhaps one would write non-technical brief summary next time, and a link to a more technical blog somewhere else. What do you think?

$ 0.00
2 years ago

Yeah, I agree. If you ask me I love the fact that we have someone here that have advanced knowledge(whatever the field), in fact, for instance I even searched read.cash for java tutorials 😂 as i like reading articles, but must keep another factor in mind that, technical audience is much less in number than the non-technical( no matter what the field). So the conclusion is that you are Right. Sure do work on technical articles as well but prefer non-technical(at least for read.cash). Stay happy, Stay blessed.

$ 0.00
2 years ago