Creatin' and Implementin' Addresses!

Buenos días friends! It’s all well and good to be able to send and receive funds by usin’ transactions, but without somewhere to store the funds yer transferrin’, well, there ain’t much use in doin’ much of anythin’!

Addresses play an integral role in the world of crypto and allow funds to be transferred accordingly. When you send a transaction, you want to make sure that the funds reach the intended recipient, and this is exactly what addresses are fer! They allow network participants to securely send funds directly to the party (or parties) whenever they deem it necessary. It also allows an end user to receive funds from other network participants.

Now Bearmint has pretty much everythin’ you need to build and implement addresses. Yep, Buckley and the Bearmint Gang have thought of every conceivable requirement and have taken active steps to ensure all yer cryptographic needs are duly catered for! We certainly wouldn’t want out users to get frustrated or lose their funds, and we know just how important it is to provide developers with a range of options when it comes to the addresses they can build and implement.

We really don’t like bein’ restrictive. No sir, we want anyone usin’ Bearmint to be able to implement the solutions they need to get the job done - we’re just here to make the process easier and provide you with some readily available options to cut yer development process time down to a mere fraction of what it would be without assistance! If what we currently have doesn’t quite meet your unique use-case, no worries at all - the followin’ guide will demonstrate how to build and implement your own address algorithm, and if you get stuck or confused, just give us a holler and we’ll be more than happy to help you figure things out!

I Need Somewhere to Send This!

Bearmint comes with a wide variety of address algorithms out of the box, but if you need to implement your own, you may do so. This guide outlines the steps required to implement bech32m (also known as BIP350) addresses. However, before we get stuck into the details, you should first take some time to familiarize yourself with a few important definitions.

What in the Sam Hill Does All That Mean?

  • @bearmint/bep9 is an NPM package that offers an implementation of BEP-009 which contains a set of utilities to deal with particularly tedious tasks that do not fall within a single domain
  • @bearmint/bep13 is an NPM package that offers an implementation of BEP-013 which allows for the sharing of a set of types between all the modules that exist within Bearmint
  • @bearmint/bep16 is an NPM package that offers an implementation of BEP-016 which contains a standardized method of bootstrapping the plugins and application via service providers
  • @bearmint/bep19 is an NPM package that offers an implementation of BEP-019 which explains how protocol buffers will apply in order to provide a standard method of serialization and deserialization, and
  • @scure/base is an NPM package that offers concrete implementations of bech32, base64, base32, base16 & base58
@scure/base is a truly magnificent package by Paul Miller. Take a gander if you need to work with any of those encodings and care about security and a minor dependency footprint!

Creating the Address Entity

You’ll first need to create a function that produces an address. This function will accept and prepend a prefix to an address and then become decoded - this output is the data that constitutes an address.

function makeAddress(prefix: string, decoded: Bech32DecodedWithArray): Address {
// This is where we'll put all of our functions!
}

Converting an Address to a Buffer

Most operations that take place internally within Bearmint require the use of buffers. For this reason, we need some way of returning the address as a buffer. Note that we call arrayBufferToBytes to return an instance of the node.js Buffer instead of the Uint8Array from JavaScript.

function toBytes() {
return arrayBufferToBytes(decoded.bytes);
}

Converting an Address to a String

Clients like wallets require some way of displaying addresses to end-users in a human-readable format. The following function is responsible for converting an address to said format.

function toString() {
return bech32m.encode(decoded.prefix, bech32m.toWords(decoded.bytes));
}

Verifying the Integrity of an Address

When validating transactions or end-user inputs, you need to ensure their validity in accordance with the requirements of your algorithm. The following function is responsible for this and should return a boolean to indicate the result.

async function verify() {
return decoded.prefix === prefix;
}

Wrapping up the Address Entity

By combining all of the aforementioned elements, the following will result - this is now ready for use in the subsequent steps when we create the actual factory that will call this. Bearmint will call the factory (which will become bound to the container) whenever it needs to interact with an address.

function makeAddress(prefix: string, decoded: Bech32DecodedWithArray): Address {
return {
toBytes() {
return arrayBufferToBytes(decoded.bytes);
},
toString() {
return bech32m.encode(decoded.prefix, bech32m.toWords(decoded.bytes));
},
async verify() {
return decoded.prefix === prefix;
},
};
}

Setting up the Factory Skeleton

Now that an address entity exists, we need something that allows Bearmint to call it. This factory function will accept a prefix that will become prepended to an address and keyPairFactory, which is the factory responsible for creating key pairs.

function makeAddressFactory({
keyPairFactory,
prefix,
}: {
prefix: string;
keyPairFactory: KeyPairFactory;
}): AddressFactory {
// This is where we'll put all of our functions!
}

Creating an Address From a Buffer

Sometimes Bearmint already has the address available as a buffer but still needs access to the functions of an address (like verify to make sure it's a valid address) and this is exactly where this function comes into play.

async function fromBytes(buffer: Buffer) {
return makeAddress(prefix, {
bytes: buffer,
prefix,
words: [],
});
}

Creating an Address From a Mnemonic

Clients such as wallets typically require that end-users input their mnemonic to execute a particular action (like signing a transaction for instance). In such cases, they may need to derive an address from said mnemonic. The following function takes care of this.

async function fromMnemonic(mnemonic: string) {
return fromPublicKey((await keyPairFactory.fromMnemonic(mnemonic)).toPublicKey());
}

Creating an Address From a Private Key

As one client may require a mnemonic, another more granular one may prefer to make use of the private key itself. In such instances, the following function will handle this.

async function fromPrivateKey(privateKey: PrivateKey) {
return fromPublicKey(privateKey.toPublicKey());
}

Creating an Address From a Public Key

In the event a client can only access a public key but wishes to derive the address for said key, it can call this function and obtain the address accordingly.

async function fromPublicKey(publicKey: PublicKey) {
return makeAddress(prefix, {
bytes: publicKey.toBytes(),
prefix,
words: [],
});
}

Creating an Address From a String

This is the most commonly used function within Bearmint because, upon receiving a transaction, the address is stored as a string within it. This is called in order to convert it into an address entity so that Bearmint can process it further.

async fromString(address: string) {
return makeAddress(prefix, bech32m.decodeToBytes(address));
}

Wrapping up the Address Factory

By combining all of the aforementioned, the following will result - this is now ready for use in the subsequent steps when we create the actual factory that will call this. Bearmint will call the factory (which will become bound to the container) whenever it needs to interact with an address.

function makeAddressFactory({
keyPairFactory,
prefix,
}: {
prefix: string;
keyPairFactory: KeyPairFactory;
}): AddressFactory {
async function fromBytes(buffer: Buffer) {
return makeAddress(prefix, {
bytes: buffer,
prefix,
words: [],
});
}
async function fromPublicKey(publicKey: PublicKey) {
return makeAddress(prefix, {
bytes: publicKey.toBytes(),
prefix,
words: [],
});
}
return {
fromBytes,
async fromMnemonic(mnemonic: string) {
return fromPublicKey((await keyPairFactory.fromMnemonic(mnemonic)).toPublicKey());
},
async fromPrivateKey(privateKey: PrivateKey) {
return fromPublicKey(privateKey.toPublicKey());
},
fromPublicKey,
async fromString(address: string) {
return makeAddress(prefix, bech32m.decodeToBytes(address));
},
};
}

Combining the Entity and Factory

Now that we’ve created a means by which to produce an address entity and a factory that assists in creating said entity from various sources (and subsequently combining them), the following code is the result. However, one step remains - registering it with Bearmint for it to use everything produced thus far.

import { Address, AddressFactory, KeyPairFactory, PrivateKey, PublicKey } from '@bearmint/bep13';
import { arrayBufferToBytes } from '@bearmint/bep19';
import { Bech32DecodedWithArray, bech32m } from '@scure/base';
function makeAddress(prefix: string, decoded: Bech32DecodedWithArray): Address {
function toString() {
return bech32m.encode(decoded.prefix, bech32m.toWords(decoded.bytes));
}
return {
toBytes() {
return arrayBufferToBytes(decoded.bytes);
},
toString,
async verify() {
return decoded.prefix === prefix;
},
};
}
function makeAddressFactory({
keyPairFactory,
prefix,
}: {
prefix: string;
keyPairFactory: KeyPairFactory;
}): AddressFactory {
async function fromBytes(buffer: Buffer) {
return makeAddress(prefix, {
bytes: buffer,
prefix,
words: [],
});
}
async function fromPublicKey(publicKey: PublicKey) {
return makeAddress(prefix, {
bytes: publicKey.toBytes(),
prefix,
words: [],
});
}
return {
fromBytes,
async fromMnemonic(mnemonic: string) {
return fromPublicKey((await keyPairFactory.fromMnemonic(mnemonic)).toPublicKey());
},
async fromPrivateKey(privateKey: PrivateKey) {
return fromPublicKey(privateKey.toPublicKey());
},
fromPublicKey,
async fromString(address: string) {
return makeAddress(prefix, bech32m.decodeToBytes(address));
},
};
}

Registering With the Application

In the final step, we simply bind our newly-created factory function to the container as ContainerType.AddressFactory. This concludes the process - Bearmint will now use the address factory whenever it needs to interact with an address.

import { readPackageJson } from '@bearmint/bep9';
import { ContainerType, Cradle, ServiceProvider } from '@bearmint/bep13';
import { makeServiceProviderSkeleton } from '@bearmint/bep16';
import { makeAddressFactory } from './factory';
function makeServiceProvider(cradle: Cradle): ServiceProvider {
const { name } = readPackageJson(__dirname);
return {
...makeServiceProviderSkeleton(__dirname),
async register() {
cradle.Container.bind(ContainerType.AddressFactory).toFunction((cradle: Cradle) =>
makeAddressFactory({
keyPairFactory: cradle.KeyPairFactory,
prefix:
cradle.RootState.getMilestone<Record<string, { prefix: string }>>().parameters
.serviceProvider[name].prefix,
}),
);
},
};
}

Mission Accomplished Y’all!

Tarnation that was a whol heapin’ load of code amigos! Hopefully you now have a decent idea about how to go about buildin’ and implementin’ addresses. It may take a little while, but it’s well worth it in the end! And again, if you need any assistance whatsoever with yer own implementations, ol’ Buck and all of us here at Bearmint will do everythin’ possible to make sure yer application works exactly as you’d expect it to!