Slopes - The AVA Platform JavaScript Library
Overview
Slopes is a JavaScript Library for interfacing with the AVA Platform. It is built using TypeScript and intended to support both browser and Node.js. The Slopes library allows one to issue commands to the AVA node APIs.
The APIs currently supported by default are:
- The AVA Virtual Machine (AVM) API
- The Keystore API
- The Admin API
- The Platform API
Getting Started
We built Slopes with ease of use in mind. With this library, any Javascript developer is able to interact with a node on the AVA Platform who has enabled their API endpoints for the developer's consumption. We keep the library up-to-date with the latest changes in the AVA Platform Specification.
Using Slopes, developers are able to:
- Locally manage private keys
- Retrieve balances on addresses
- Get UTXOs for addresses
- Build and sign transactions
- Issue signed transactions to the AVM
- Create a subnetwork
- Administer a local node
- Retrieve AVA network information from a node
The entirety of the Slopes documentation can be found on our XXX Fix Link XXX Slopes documentation page.
Requirements
Slopes requires Node.js LTS version 12.13.1 or higher to compile.
Slopes depends on the following two Node.js modules internally, and we suggest that your project uses them as well:
- Buffer: Enables Node.js's Buffer library in the browser.
- BN.js: A bignumber library for Node.js and browser.
Both of the above modules are extremely useful when interacting with Slopes as they are the input and output types of many base classes in the library.
Installation
Slopes is available for install via npm
:
npm install --save slopes
You can also pull the repo down directly and build it from scratch:
npm run build
This will generate a pure javascript library and place it in a folder named "dist" in the project root. The "slopes.js" file can then be dropped into any project as a pure javascript implementation of Slopes.
The Slopes library can be imported into your existing Node.js project as follows:
const slopes = require("slopes");
Or into your TypeScript project like this:
import * as slopes from "slopes"
Importing essentials
import * as slopes from "slopes";
import BN from 'bn.js';
import { Buffer } from 'buffer/';
let bintools = slopes.BinTools.getInstance();
The above lines import the libraries used in the below example:
- slopes: Our javascript module.
- bn.js: A bignumber module use by Slopes.
- buffer: A Buffer library.
- BinTools: A singleton built into Slopes that is used for dealing with binary data.
Example 1 — Managing AVM Keys
Slopes comes with its own AVM Keychain. This keychain is used in the functions of the API, enabling them to sign using keys it's registered. The first step in this process is to create an instance of Slopes connected to our AVA Platform endpoint of choice.
let mynetworkID = 12345;
let ava = new slopes.Slopes("localhost", 9650, "https", mynetworkID);
let avm = ava.AVM();
Accessing the keychain
The keychain is accessed through the AVM API and can be referenced directly or through a reference varaible.
let myKeychain = avm.keyChain();
This exposes the instance of the class AVM Keychain which is created when the AVM API is created. At present, this supports secp256k1 curve for ECDSA key pairs.
Creating AVM key pairs
The keychain has the ability to create new keypairs for you and return the address assocated with the key pair.
let newAddress1 = myKeychain.makeKey();
You may also import your exsting private key into the keychain using either a Buffer...
let mypk = Buffer.from("330530eda3225d280d42efc5f02d31d122da3da3093c739ddd7d16612c7dfd53", "hex");
let newAddress2 = myKeychain.importKey(mypk);
... or an AVA serialized string works, too:
let mypk = "2LvB5YuMA4F4fFeeYDFbFxWCNBBW1317L";
let newAddress2 = myKeychain.importKey(mypk);
Working with keychains
The AVMKeyChain extends the global KeyChain class, which has standardized key management capabilities. The following functions are available on any keychain that implements this interface.
let addresses = myKeychain.getAddresses();
let exists = myKeychain.hasKey(myaddress);
let keypair = myKeychain.getKey(myaddress);
Working with keypairs
The AVMKeyPair class implements the global KeyPair class, which has standardized keypair functionality. The following operations are available on any keypair that implements this interface.
let addresses = avm.keyChain().getAddresses();
let pubk = keypair.getPublicKey();
let pubkstr = keypair.getPublicKeyString();
let privk = keypair.getPrivateKey();
let privkstr = keypair.getPrivateKeyString();
keypair.generateKey();
let mypk = Buffer.from("263956e2a04bef77ff8c4834a1c26158e28e040c7621c89c5feab1b3054ec699", "hex");
let successul = keypair.importKey(mypk);
let message = "Wubalubadubdub";
let signature = keypair.sign(message);
let signerPubk = keypair.recover(message, signature);
let isValid = keypair.verify(message, signature, signerPubk);
Example 2 — Creating An Asset
This example creates an asset in the AVM and publishes it to the AVA Platform. The first step in this process is to create an instance of Slopes connected to our AVA Platform endpoint of choice.
let mynetworkID = 12345;
let ava = new slopes.Slopes("localhost", 9650, "https", mynetworkID);
let avm = ava.AVM();
Describe the new asset
The first steps in creating a new asset using Slopes is to determine the qualties of the asset. We want to mint an asset with 100,000 coins and issue it to three known addresses we control. We require two of the three addresses to spend this output.
Note: This example assumes we have the keys already managed in our AVM Keychain. We are explicit with the addresses to demonstrate serialization.
let amount = new BN(400)
let address1 = "c3344128e060128ede3523a24a461c8943ab0859";
let address2 = "51025c61fbcfc078f69334f834be6dd26d55a955";
let address3 = "B6D4v1VtPYLbiUvYXtW4Px8oE9imC2vGW";
let addresses = [
bintools.avaSerialize(Buffer.from(address1, "hex")),
bintools.avaSerialize(Buffer.from(address2, "hex")),
address3
];
let threshold = 2;
Creating the signed transaction
Now that we know what we want an asset to look like, we create an output to send to the network. This process is fairly straight-forward:
- We instantiate an output of the type "Create Asset" populated with the desired parameters.
- We create an unsigned transaction from an array of inputs and outputs.
- Inputs come from the UTXOs and are spent in a transaction.
- In this case there are no unspent assets, so the input array is blank.
- We use our AVM Keychain to sign and return the signed transaction.
let output = new slopes.OutCreateAsset(amount, addresses, undefined, threshold);
let networkID = ava.getNetworkID();
let blockchainID = bintools.avaDeserialize(ava.AVM().getBlockchainID());
let unsigned = new slopes.TxUnsigned([], [output], networkID, blockchainID);
let signed = avm.keyChain().signTx(unsigned);
Issue the signed transaction
Now that we have a signed transaction ready to send to the network, let's issue it!
Using the Slopes AVM API, we going to call the issueTx function. This function can take either the Tx class returned in the previous step, a base-58 string AVA serialized representation of the transaction, or a raw Buffer class with the data for the transaction. Examples of each are below:
let txid = await avm.issueTx(signed);
let txid = await avm.issueTx(signed.toString());
let txid = await avm.issueTx(signed.toBuffer());
We assume ONE of those methods are used to issue the transaction.
Get the status of the transaction
Now that we sent the transaction to the network, it takes a few seconds to determine if the transaction has gone through. We can get an updated status on the transaction using the TxID through the AVM API.
let status = await avm.getTxStatus(txid);
The statuses can be one of "Accepted", "Processing", "Unknown", and "Rejected":
- "Accepted" indicates that the transaction has been accepted as valid by the network and executed
- "Processing" indicates that the transaction is being voted on.
- "Unknown" indicates that node knows nothing about the transaction, indicating the node doesn't have it
- "Rejected" indicates the node knows about the transaction, but it conflicted with an accepted transaction
Identifying the newly created asset
The AVM uses the TxID of the transaction which created the asset as the unique identifier for the asset. This unique identifier is henceforth known as the "AssetID" of the asset. When assets are traded around the AVM, they always reference the AssetID that they represent.
Example 3 — Sending An Asset
This example sends an asset in the AVM to a single recipient. The first step in this process is to create an instance of Slopes connected to our AVA Platform endpoint of choice.
let mynetworkID = 12345;
let ava = new slopes.Slopes("localhost", 9650, "https", mynetworkID);
let avm = ava.AVM();
We're also assuming that the keystore contains a list of addresses used in this transaction.
Getting the UTXO Set
The AVM stores all available balances in a datastore called Unspent Transaction Outputs (UTXOs). A UTXO Set is the unique list of outputs produced by transactions, addresses that can spend those outputs, and other variables such as lockout times (a timestamp after which the output can be spent) and thresholds (how many signers are required to spend the output).
For the case of this example, we're going to create a simple transaction that spends an amount of available coins and sends it to a single address without any restrictions. The management of the UTXOs will mostly be abstracted away.
However, we do need to get the UTXO Set for the addresses we're managing.
let myAddresses = avm.keyChain().getAddresses();
let utxos = await avm.getUTXOs(myAddresses);
Spending the UTXOs
The makeUnsignedTx()
helper function sends a single asset type. We have a particular assetID whose coins we want to send to a recipient address. This is an imaginary asset for this example which we believe to have 400 coins. Let's verify that we have the funds available for the transaction.
let assetid = "23wKfz3viWLmjWo2UZ7xWegjvnZFenGAVkouwQCeB9ubPXodG6";
let mybalance = utxos.getBalance(myAddresses, assetid);
We have 400 coins! We're going to now send 100 of those coins to our friend's address.
let sendAmount = new BN(100);
let friendsAddress = "B6D4v1VtPYLbiUvYXtW4Px8oE9imC2vGW";
let unsignedTx = avm.makeUnsignedTx(utxos, amount, [friendsAddress], myAddresses, myAddresses, assetid);
let signedTx = avm.signTx(unsignedTx);
let txid = await avm.issueTx(signedTx);
And the transaction is sent!
Get the status of the transaction
Now that we sent the transaction to the network, it takes a few seconds to determine if the transaction has gone through. We can get an updated status on the transaction using the TxID through the AVM API.
let status = await avm.getTxStatus(txid);
The statuses can be one of "Accepted", "Processing", "Unknown", and "Rejected":
- "Accepted" indicates that the transaction has been accepted as valid by the network and executed
- "Processing" indicates that the transaction is being voted on.
- "Unknown" indicates that node knows nothing about the transaction, indicating the node doesn't have it
- "Rejected" indicates the node knows about the transaction, but it conflicted with an accepted transaction
Check the results
The transaction finally came back as "Accepted", now let's update the UTXOSet and verify that the transaction balance is as we expected.
Note: In a real network the balance isn't guaranteed to match this scenario. Transaction fees or additional spends may vary the balance. For the purpose of this example, we assume neither of those cases.
let updatedUTXOs = await avm.getUTXOs();
let newBalance = updatedUTXOs.getBalance(myAddresses, assetid);
if(newBalance.toNumber() != mybalance.sub(sendAmount).toNumber()){
throw Error("heyyy these should equal!");
}