From Solidity to Ink!, a practical take

Wiktor Starczewski
4 min readFeb 28, 2021

--

Photo by Sam Albury on Unsplash

There’s many ways to get your smart contracts going on Polkadot. Moonbeam and potentially other chains will offer EVM compatibility so that certainly can be a route for some folks who wish to keep their Solidity codebase intact and do not want to be forced to pick up another skill. But what skill is it, that would enable us to use the Polkadot native smart contract platform, Ink!, and do away with Solidity and its many disadvantages? Well, the language of choice is Rust, since it is, and probably will remain, the one with most support in terms of tooling and documentation. I went through the transition from Solidity to Ink!/Rust in the past 2 weeks for a project I’m working on, and I will try to unpack the transition in a deliberately as-short and as-concise as possible manner.

Hello, pointers

Having a couple of years of C++ in my history, it wasn’t so much of an issue, but I do recognise the need for a bit of a mental switch. The compiler does a good job explaining why you’re wrong when you are and you will be, from time to time. What threw me off in the beginning a little bit was that Rust does away with C++’s -> operator, and instead will try to do the right thing if possible by itself. It makes sense, but I found it unexpected and, honestly with pointers, unexpected is bad.

Hello, traits

Ok, so traits are like interfaces right? Well, sure, but they also can carry default implementations with them. So the first time the compiler complained about PackedLayout trait not being satisfied for my structs, which by the way I pass around like magic across all my functions and contracts (and no experimental pragmas either, Solidity! ;)), I was sure I was gonna have to do some implementing, but I was happy to find all I need to do is derive a trait for my struct (basically decorate it with an expression), and suddenly the default implementations were kicking in for free. It is something that can be overwhelming at first, but it makes a lot of sense and when in doubt, just follow the compiler on this one. On the plus side, serialisation and deserialisation works like a charm when you finally get the compiler errors to go away.

#[derive(Encode, Decode, SpreadLayout, PackedLayout, Clone)]
#[cfg_attr(
feature = "std",
derive(
Debug,
PartialEq,
Eq,
scale_info::TypeInfo,
ink_storage::traits::StorageLayout
)
)]
pub struct MyStruct {
foo: u32,
}

std and the ink modules

Rust as a language is relatively lean, and relies on a module called std to add more sugar to the experience, including naming basic types, like Vec, HashMap, etc. The catch here is that Ink! does not work with the standard std library, instead opting for providing its own equivalent. While semantically very similar, there are minor differences that are worth taking account when working with Ink!. It’s best to always use the main documentation as a starting point, and while it can at some points redirect to the official Rust documentation, it’s best to get back as soon as possible, because not everything is supported and one can potentially lose time here.

Collections and iterators

Collections effectively come in two flavours, one is defined in ink_storage for things that should be saveable to the contracts storage, and another flavour that can not be used for this purpose. So a Vec, is not a Vec, is not a Vec. All depends where it comes from.

The storage collections support iterators, and they should really be used, as they simplify the code tremendously. They’re also quite intuitive, but certainly require a bit getting used to. Thankfully chaining is a well established concept, so it’s not that hard to get to a stage where it’s possible to do a bit crazier stuff. That said, it took me a little bit, with very little Rust to my name, to come up with this code, which folds a HashMap, indexed by a (AccountId, u8) tuple to a custom struct, into a Vec of (u8, the struct) tuples, filtered by the address of the caller (but hey, it worked in the end):

#[ink(message)]
pub fn get_commanders(&self, caller: AccountId) -> Vec<(u8, CommanderData)> {
self.commanders
.iter()
.filter_map(|entry| {
let (&key, &value) = entry;
let (account, commander_id) = key;
if account == caller {
Some((commander_id, value))
} else {
None
}
})
.collect()
}

where

commanders: StorageHashMap<(AccountId, u8), CommanderData>pub struct CommanderData {
xp: u32,
}

Elegant and quite powerful in the end.

Option, Result, Some, None, match and unwrap

All of those need to be understood, because they’re used aplenty. Rust does not have null references (so also no null reference exceptions ;)), so it uses wrapper types to facilitate cases where something just does not exist, or has errored (and therefore does not exist). match will allow unpacking them switch-style, and unwrap will effectively force the picking of the contained object, and panic if it fails (but it can be chained with fallback methods).

match lhs_moves {
Some(ref mut moves) => self.i_have_the_moves(moves),
_ => ()
}

And for the unwrap,

defences: StorageHashMap<AccountId, MyCrazyStruct>,let defence: &MyCrazyStruct = self.defences.get(&self.env().caller()).unwrap();

Overall, a verdict after two weeks

There is certainly a bit of a learning curve to Rust/Ink!. I’d certainly start here, and keep it open together with the more apidoc style documentation here. I tried to take the Rust-first, Ink-second approach initially, and I found it to be subpar to just letting Ink! documentation fall back on Rust one when it needs to. That said, it’s important to know that there are differences between Rust and its Ink! de facto implementation.

Next step, polkadot.js to wire up my contracts to the frontend.

--

--

Wiktor Starczewski
Wiktor Starczewski

Written by Wiktor Starczewski

15+ years professional experience in software developement. JavaScript and Blockchain guy.

No responses yet