Near Api JS with Rails 7
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)
Go to
config/importmap.rb
and add the following:pin_all_from "app/javascript/custom", under: "custom"
Go to
app/javascript/application.js
file and add the following:import "custom/main"
In
app/javascript
directory, addcustom
folder.In
app/javascript/custom
directory add your custom js filemain.js
.Run In your terminal:
rails assets:precompile
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 thosewindow.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!
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).