Connect with us

Technology

Extending Flarum: Including a Web3 Handle to a Person’s Profile – SitePoint


In our first Flarum tutorial — “Writing a Flarum Extension: Constructing a Customized Subject” — we lined the way to add a brand new customized discipline to a person’s profile in a blazing quick and very extensible open-source discussion board software program known as Flarum. The sphere we added was web3address, the account of a person’s Web3 identification.

On this second tutorial, we take issues a step additional by permitting customers so as to add a Web3 tackle to their profile.

Notice ℹ: The Web3 ecosystem is a brand new web of decentralized internet hosting, self-owned knowledge, and censorship-resistant communication. For a primer on Web3, please see this 15 minute speak at FOSDEM.

Cryptographically Including Web3

At this level, our customized discipline works, is settable by customers, and is editable by directors of the discussion board. Now let’s make certain customers can add their tackle in a cryptographically sound manner.

This implies a person will solely have the ability to add an tackle they’ve confirmed to personal. You show possession of an tackle by signing a message with that tackle’ non-public key. Solely the one who has the non-public key of a public–non-public keypair is taken into account to be the proprietor of that keypair. The general public a part of a public–non-public keypair is the bottom from which a Web3 tackle is mathematically derived.

To personal some tackle(es), a person ought to set up the Polkadot JS extension and create an account. The UI needs to be self-explanatory, however there’s a extra detailed information right here if wanted.

The sphere we added within the earlier tutorial at the moment lets customers manually set any worth, however this implies customers can enter anybody’s tackle and even some gibberish. We wish them to solely add their very own, so we’ll change it with a button that can:

  • ask for permission to entry the browser extension containing the account(s)
  • load the accounts and supply a dropdown to pick certainly one of them
  • ask the person to signal a message with that tackle and confirm that signature
  • register that account because the person’s Web3 tackle

Let’s dive in.

Button

First we have to change our Web3 enter discipline right into a Dropdown. Let’s create elements/Web3Dropdown.js:

import Part from "flarum/Part";
import Dropdown from "flarum/elements/Dropdown";

export default class Web3Dropdown extends Part {
  view() {
    return (
      <Dropdown
        buttonClassName="Button"
        onclick={this.handleClick.bind(this)}
        label="Add Web3 Account"
      >
      </Dropdown>
    );
  }

  handleClick(e) {
    console.log("Choose one thing");
  }
}

We create a brand new part within the type of Web3Field.js we created earlier, however now we return an occasion of the Dropdown part. The Dropdown part is certainly one of a number of normal JS elements in Flarum. You’ll find a full record right here. We additionally give it the category “Button” to match its type with the remainder of the discussion board. On click on, we print a message.

The part is a button with the flexibility to summon a dropdown from passed-in objects, very like the “Controls” menu that an admin of the discussion board can see on a person’s profile:

Dependencies

In our extension’s JS folder, we’ll add two dependencies:

yarn add @polkadot/util-crypto @polkadot/util @polkadot/extension-dapp

Notice ⚠: don’t overlook to cease the method should you’re nonetheless operating yarn dev and don’t overlook to begin it once more after having put in these dependencies!

util-crypto comprises some utility capabilities for cryptographic operations.util comprises some fundamental utilities, like turning strings into bytes and so on. (There are docs for each right here.) extension-dapp is a helper layer that lets the JS we write work together with the Polkadot JS extension we’ve put in. (Go to the docs right here.)

Asking Permission and Getting Accounts

Let’s modify our Dropdown now to ask the person for permission to entry their Web3 accounts:

  import { web3Accounts, web3Enable } from "@polkadot/extension-dapp";

  

  async handleClick(e) {
    await web3Enable("Flarum Web3 Handle Extension");
    const accounts = await web3Accounts();
    console.log(accounts);
  }

Discover that we modified the handleClick perform to be async! We want this to have the ability to await guarantees within the code. In any other case, we’d get caught with nesting then calls.

First we name web3Enable, which asks us for permission to entry the extension. Then we seize all of a person’s accounts and output them within the console. When you have the Polkadot JS extension put in and a few accounts loaded, be at liberty to do that out now.

Authorize or reject

Output in the console

However what if somebody doesn’t have the extension put in? We might have an admin-level setting which lets us select whether or not to cover the button if the extension isn’t round or to redirect the person to its URL, however for now, let’s select the latter:

  import { web3Accounts, web3Enable, isWeb3Injected } from "@polkadot/extension-dapp";

  

  async handleClick(e) {
    await web3Enable("Flarum Web3 Handle Extension");
    if (isWeb3Injected) {
      const accounts = await web3Accounts();
      console.log(accounts);
    } else {
      window.location = "https://github.com/polkadot-js/extension";
    }
  }

Selecting the Account

The following step is letting the person choose an account and on the identical time asking them to signal a message with it. As soon as verified, that signature irrefutably proves possession of the account.

The Dropdown part takes an objects array of things to show. That is mostly an array of Button parts, the place Button is a typical Flarum part. To present our part a component-wide knowledge property that we will manipulate and base adjustments on, we outline it in oninit:

  oninit() {
    this.web3accounts = [];
  }

As a substitute of simply console.loging the accounts, we then set the accounts to this new attribute:

this.web3accounts = accounts;
m.redraw();

Notice ⚠: we use redraw right here to make mithril (m) re-render our part. If we don’t, the part will render an empty dropdown first (it doesn’t have accounts but) and can want one other shut–open of the dropdown to indicate accounts (which triggers a redraw). We wish the accounts within the dropdown as quickly as they’re loadedrobust>, even when the dropdown is already open and has no parts, so this can do the trick. Every time it’s good to apply adjustments to your part dynamically with out UI triggers, often primarily based on some distant knowledge fetches or knowledge processing, you’re okay to make use of m.redraw().

Lastly, we make view, the perform answerable for our rendering, react to this modification:

  view() {
    const objects = [];
    if (this.web3accounts.size) {
      for (let i = 0; i < this.web3accounts.size; i++) {
        objects.push(
          <Button
            worth={this.web3accounts[i].tackle}
            onclick={this.handleAccountSelect}
          >
            {this.web3accounts[i].tackle}
            {this.web3accounts[i].meta.title
              ? ` - ${this.web3accounts[i].meta.title}`
              : ""}
          </Button>
        );
      }
    }
    return (
      <Dropdown
        buttonClassName="Button"
        onclick={this.handleClick.bind(this)}
        label="Set Web3 Account"
      >
        {objects}
      </Dropdown>
    );
  }

First we outline an empty placeholder array. Then, if there are greater than zero web3accounts saved on this part, we iterate by them to create a button for every account with the worth set to the account’s tackle and the label set to a mix of tackle and in-extension-defined label. Lastly, we move these Buttons into the Dropdown part.

We additionally have to import the Button part:

import Button from "flarum/elements/Button";

Notice ℹ: discover that we’re not binding this to every Button’s onclick occasion handler. It’s because this would change the button’s context to the mum or dad Dropdown part, moderately than the button being clicked, and would make fetching the button’s worth much less simple.

Subsequent, we have to react to the person clicking on one of many addresses within the menu:

  handleAccountSelect() {
    console.log(this.worth);
  }

Testing this can reveal that we will certainly choose a Web3 account from our extension, and that it will get logged within the console after choice.

setting the Web3 account on the profile page

view from the console

Verifying the Account

Lastly, we have to ask the person to signal a message. Let’s say the message is “Excessive possession”. This can immediate them to submit a password within the extension popup and return a signed message.

First, some imports:

import {
  web3Accounts,
  web3Enable,
  isWeb3Injected,
  web3FromAddress,  
} from "@polkadot/extension-dapp";
import { stringToHex } from "@polkadot/util"; 

web3FromAddress is a useful methodology to assemble a Web3 object, the usual object for Web3 interactions, with the given tackle because the “protagonist”. stringToHex is used to show a string right into a hexadecimal illustration, which is the info format a signer expects (bytes):

  async handleAccountSelect() {
    const tackle = this.worth;
    const web3 = await web3FromAddress(tackle);
    const signer = web3.signer;
    const hexMessage = stringToHex("Excessive possession");
    attempt {
      const signed = await signer.signRaw({
        kind: "bytes",
        knowledge: hexMessage,
        tackle: tackle,
      });
      console.log(signed);
    } catch (e) {
      console.log("Signing rejected");
      return;
    }
  }

We first flip the perform into an async one so we will use await. Then we make a web3 occasion from our tackle, as defined above, and extract the signer. The signer is a cryptographic software which can routinely extract the general public key from an tackle and signal a given message, offered in bytes. (That is what we want the hexMessage for — changing our string into bytes, represented hexadecimally.)

The one method to get signed is to signal; every part else causes an error to be thrown.

Saving the Account

Lastly, we comply with the identical course of as earlier than with Web3Field.js — move the tackle into save:

  async handleAccountSelect() {
    const tackle = this.worth;
    const web3 = await web3FromAddress(tackle);
    const signer = web3.signer;
    const hexMessage = stringToHex("Excessive possession");
    attempt {
      const signed = await signer.signRaw({
        kind: "bytes",
        knowledge: hexMessage,
        tackle: tackle,
      });
      console.log(signed);
      const person = app.session.person;
      person
        .save({
          web3address: tackle,
        })
        .then(() => m.redraw());
    } catch (e) {
      console.log("Signing rejected");
      return;
    }
  }

Notice ℹ: we add m.redraw to refresh the worth on-screen after save. The redraw will set off a refresh of the extension’s JavaScript and skim in knowledge from the Person occasion returned by the save operation, displaying our up to date tackle if the save was profitable.

Server-side Verification

That is moderately safe. Even when somebody hacks round our JS and inserts a Web3 tackle that doesn’t belong to them, they will’t actually do a lot with it. They will merely current themselves as somebody they’re not. Nonetheless, we will get round this as effectively by doing a little server-side validation.

As a result of there’s at the moment no toolkit for PHP for the cryptographic capabilities we’re utilizing, we’ll should be inventive. Particularly, we’ll write a command-line script in TypeScript which we’ll name from inside PHP.

In js/src/discussion board, create the scripts folder and add the file confirm.js:

let util_crypto = require("@polkadot/util-crypto");

util_crypto
  .cryptoWaitReady()
  .then(() => {
    const verification = util_crypto.signatureVerify(
      course of.argv[2], 
      course of.argv[3], 
      course of.argv[4] 
    );
    if (verification.isValid === true) {
      console.log("OK");
      course of.exitCode = 0;
    } else {
      console.error("Verification failed");
      course of.exitCode = 1;
    }
  })
  .catch(perform (e) {
    console.error(e.message);
    course of.exit(1);
  });

The crypto utilities package deal comprises helper strategies for every part we want. cryptoWaitReady waits for crypto operations to init — particularly, sr25519, which we’re utilizing right here, wants a bit of WASM to heat up. Then, we confirm the signature utilizing the signatureVerify perform by processing the arguments offered.

We are able to take a look at this domestically (get the values from the payload of a Save request after setting an tackle within the dropdown, or by manually signing the “Excessive possession” message in Polkadot UI):

$ node src/discussion board/scripts/confirm.js "Excessive possession" 0x2cd37e33c18135889f4d4e079e69be6dd32688a6bf80dcf072b4c227a325e94a89de6a80e3b09bea976895b1898c5acb5d28bccd2f8742afaefa9bae43cfed8b 5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB
> OK
$ node src/discussion board/scripts/confirm.js "Mistaken message" 0x2cd37e33c18135889f4d4e079e69be6dd32688a6bf80dcf072b4c227a325e94a89de6a80e3b09bea976895b1898c5acb5d28bccd2f8742afaefa9bae43cfed8b 5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB
> Verification failed

Our verification script works.

Notice ℹ: the identical message signed by the identical tackle will give a distinct hash each time. Don’t rely on them being the identical. For instance, these three payloads are “Excessive possession” signed by the identical tackle 3 instances:

// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0x0c837b9a5ba43e92159dc2ff31d38f0e52c27a9a5b30ff359e8f09dc33f75e04e403a1e461f3abb89060d25a7bdbda58a5ff03392acd1aa91f001feb44d92c85"}""
// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0x3857b37684ee7dfd67304568812db8d5a18a41b2344b15112266785da7741963bdd02bb3fd92ba78f9f6d5feae5a61cd7f9650f3de977de159902a52ef27d081"}""
// {"web3address":"5EFfZ6f4KVutjK6KsvRziSNi1vEVDChzY5CFuCp1aU6jc2nB","signedMessage":"0xa66438594adfbe72cca60de5c96255edcfd4210a8b5b306e28d7e5ac8fbad86849311333cdba49ab96de1955a69e28278fb9d71076a2007e770627a9664f4a86"}""

We additionally want to change our app.session.person.save name within the Dropdown part so it really sends the signed message to the again finish:

  person
    .save({
      web3address: tackle,
      signedMessage: signed.signature,
    })
    .then(() => console.log("Saved"));

When our web3address worth is being saved on a person, we have to intercept that operation, confirm the signature provided that it’s the person doing the save, not an admin, and save if okay, or reject (ideally with an error message) if not.

Let’s modify out deal with perform in SaveUserWeb3Address.php:

if (isset($attributes['web3address'])) {
    if (!$isSelf) {
        $actor->assertPermission($canEdit);
    }

    chdir(__DIR__ . "/../../js");
    $command = "node src/discussion board/scripts/confirm.js "Excessive possession" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    exec($command, $out, $err);

    if ($err) {
        return false;
    }
    $person->web3address = $attributes['web3address'];
    $person->save();
}

We added strains 6 to 12: we alter the listing to the one containing our verification script. Then, we compose the command-line name to the script by passing within the required params, and at last if the error code $err is something apart from falsy (will probably be 0 if all went effectively), we cease the saving course of.

This doesn’t enable admins to alter the worth at will although, so let’s add that. As per the docs, an $actor has the isAdmin helper. The ultimate model of our deal with methodology is now:

public perform deal with(Saving $occasion)
{
    $person = $occasion->person;
    $knowledge = $occasion->knowledge;
    $actor = $occasion->actor;

    $isSelf = $actor->id === $person->id;
    $canEdit = $actor->can('edit', $person);
    $attributes = Arr::get($knowledge, 'attributes', []);

    if (isset($attributes['web3address'])) {
        if (!$isSelf) {
            $actor->assertPermission($canEdit);
        }

        if (!$actor->isAdmin()) {
            chdir(__DIR__ . "/../../js");
            $command = "node src/discussion board/scripts/confirm.js "Excessive possession" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
            exec($command, $out, $err);

            if ($err) {
                return false;
            }
        }
        $person->web3address = $attributes['web3address'];
        $person->save();
    }
}

Error Readability

The very last thing we should always do is make an error extra UX-friendly if the verification of the tackle fails. A return false isn’t very helpful; the UI would merely do nothing. Since it is a validation error (we’ve didn’t validate the person’s possession of this tackle), we will throw a ValidationException:

if ($err) {
    throw new FlarumBasisValidationException(["Signature could not be verified."]);
}

Now if our verification fails, we’ll see this in a useful error message:

error message

Pre-deploy caveat

As a result of we’re in growth mode, our extension has entry to Node and Yarn and will set up the Polkadot dependencies wanted to do the cryptography. Nevertheless, in a manufacturing atmosphere there’s no straightforward method to routinely run yarn set up on a Composer-installed package deal, so our confirm script wouldn’t work with out vital person intervention. We have to bundle the confirm.js script right into a file that’s immediately runnable by NodeJS with out package deal managers. This nonetheless means our manufacturing server has to have NodeJS put in, however that’s all it wants — at the least till the cryptographic performance we use additionally seems in PHP taste.

To bundle up our script, contained in the extension’s JS folder we will run:

npx browserify src/discussion board/scripts/confirm.js > dist/confirm.js

This can run Browserify with out putting in it, bundle up all of the dependencies and output a single JS blob that we save into dist/confirm.js. Now we will commit this file into the extension’s repo and goal it if it exists. The truth is, we will make our extension detect whether or not or not the discussion board is in debug mode and goal the supply vs dist file primarily based on that flag:

if (!$actor->isAdmin()) {
    chdir(__DIR__ . "/../../js");
    if (app(FlarumBasisConfig::class)->inDebugMode()) {
        $command = "node src/discussion board/scripts/confirm.js "Excessive possession" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    } else {
        $command = "node dist/confirm.js "Excessive possession" " . $attributes['signedMessage'] . " " . $attributes['web3address'] . " 2>&1";
    }
    exec($command, $out, $err);

    if ($err) {
        throw new ValidationException(["Signature could not be verified."]);
    }
}

Our Listener will learn the supply model if the inDebugMode returns true, or dist/confirm.js in any other case.

Conclusion

Our discussion board customers can now add their Web3 addresses to their profile. You’ll find the printed extension at swader/web3address.

As a result of some customers may not be utilizing Chrome or Firefox and received’t have the extension obtainable, and since some may desire various account technology strategies as documented right here, we enable directors to manually enter addresses for particular person accounts, offered these customers show possession of their tackle. As that’s a handbook course of, nevertheless, the proving course of is exterior the scope of this tutorial.

This lays the muse for future use of those addresses. They’ll be used to pay out participation factors — crypto tokens — for person exercise on the discussion board, encouraging energetic discussions and high quality engagement. We’ll see about doing that in a subsequent information.

Bought any suggestions about this publish? Want one thing clarified? Be happy to contact the writer.



Click to comment

Leave a Reply

Your email address will not be published. Required fields are marked *