Limiting Supply
The supply of a jig class may be limited through the use of a minter. The minter is a separate jig or jig class that regulates the number of mintee jigs produced. In the example on the right, the Weapon jig class is the minter for its own instances. We use the special caller variable to enforce that Weapon jigs may only be created through the class's mint() method. If a player tried to create a Weapon instance using new Weapon() outside of mint(), Run would throw an error because the caller property would be null.
class Weapon extends Jig {
init(owner) {
// Only the Weapon class can mint Weapon jigs
expect(caller).toBe(Weapon)
this.owner = owner
}
static mint() {
if (this.supply >= 10) throw new Error('too many weapons')
// The class keeps track of the number of mints
this.supply++
return new Weapon()
}
}
Weapon.supply = 0
Weapon.deps = { expect: Run.extra.expect }
const weapon = Weapon.mint()
Random Numbers
Many applications use random numbers to perform updates. For example, you might wish to randomly generate the properties of a digital pet when it is created, or you may wish to determine the winner of a game using a statistical calculation. Generating random numbers inside jigs can be challenging because blockchains have to be deterministic. Run disables the Math.random() function within jigs for this reason.
It may be tempting to use the location or origin properties of a jig as random numbers. Certainly, they appear random. However, this is a bad idea because a motivated user would be able to make alterations to their Bitcoin transactions, either during signing or payment, until they get the location or origin that leads to the random value they want.
Instead, we recommended using an oracle to provide randomness. Jigs that require random values would send a paid request to an oracle service. This oracle service would set the random number on the request jigs it receives, and when the original jig that requires randomness is synced, it will get the random value.
This same idea also applies to other oracle data, including dates, prices, and more.
Use an oracle to provide random values
class RandomValue {
init() {
this.owner = ORACLE_ADDRESS
this.satoshis = 1000
}
set(value) {
this.value = value
this.satoshis = 0
this.destroy()
}
}
class DigitalPet {
init() {
this.random = new RandomValue()
}
setup() {
this.size = this.random.value % 10
this.agility = this.random.value % 100
this.color = this.random.value % 5
}
}
DigitalPet.deps = { RandomValue }
Dynamic Whitelists
An app may want to allow third-party developers to create new classes of jigs. Those jigs can freely interact with existing jigs once approved. For this, you can create a dynamic whitelist of approved classes that jigs can check. See the example to the right 🠮
Note: Dynamic blacklists pose more challenges than whitelists. Solutions are coming soon.
A dynamic whitelist to support new tokens over time
class SupportedClasses {
init() { this.classes = new Set() }
add(T) { this.classes.add(T) }
}
const whitelist = new SupportedClasses()
class Game {
init() { this.items = new Set() }
use(jig) {
expect(Game.whitelist.classes.has(jig.constructor)).toBe(true)
this.items.add(jig)
}
}
Account.whitelist = whitelist
Atomic Swaps
Run supports atomically updating jigs with different owners using the Transaction API. One use case for this is atomic swaps, where two jigs owned by different users are exchanged in a single transaction.
The general process for an atomic swap is for one user to start a Run transaction by calling new Run.Transaction(). Then, this user performs all updates inside an update() call on the transaction, including calling methods on jigs they don't currently own. Run allows this user to build the transaction even if they won't be able to sign for every jig. Finally, this first user calls tx.export() to export the transaction, which will pay and sign for it in the process.
The transaction is now built and must now be handed to other parties for them to sign. The other party then calls run.import() to load the transaction they received. They may then want to check the transaction by inspecting tx.outputs and Run.util.metadata. If they approve, then they may call tx.publish() to sign and publish. If more signatures are needed, they can export the transaction instead of publishing.
The Transaction API may also be used to simulate state channels and propose changes to other jigs. See the Transaction API for more information.
Atomically swapping two items with different owners
Machine 1
const mine = await run.load(myLocation)
const theirs = await run.load(theirLocation)
const me = mine.owner
const them = theirs.owner
const tx = new Run.Transaction()
tx.update(() => mine.send(them))
tx.update(() => theirs.send(me))
const rawtx = await tx.export()
Machine 2
const tx = await run.import(rawtx)
if (tx.outputs.length) {
// TODO: Inspect the transaction to be sure it atomically swaps
}
await tx.publish({ pay: false })
Custom purses
Invisible Money Button
MoneyButton provides a simple module to fund arbitrary bitcoin transactions using Invisible Money Button.
In a web browser, include the following script tag:
Upon setup and granting permission to Invisible Money Button, when creating Run transactions, the user's MoneyButton wallet will be used to pay for the transactions.
Example setup in the browser:
const getPermissionForCurrentUser = () => { return localStorage.token }
const savePermissionToken = token => { localStorage.setItem('token', token) }
const imb = new moneyButton.IMB({
clientIdentifier: 'your-moneybutton-client-identifier',
permission: getPermissionForCurrentUser(),
onNewPermissionGranted: token => savePermissionToken(token)
});
const purse = new MoneyButtonPurse.MBPurse(imb, bsv);
const run = new Run({
owner: 'your-owner-private-key',
purse
});
Using Run on a Server
You may want to use Run on your server. For example, to:
Issue tokens to users who send you BSV
Index user jigs to allow searching in your app
Take actions automatically as in a game server
Gather statistics about your user's behaviors
Perform administrative tasks like managing a blacklist
Run is designed to work in a Node.js server as easily as a web browser. You might create a REST API using express that allows users to make queries. Or you might deploy a serverless function that responds to events. Or you might listen to relevant transactions using WhatsOnChain's SSE events to perform actions automatically. Run supports all of these architectures.
Run requires at least version 10 of Node. You can check which version you are using with node --version. If you are using Google Functions, be sure to set { "engine": 10 } in your package.json since it defaults to Node 8.
Here are a few more tips:
Expand the In-Memory Cache
The LocalCache stores jig state and blockchain transactions in memory. It is the default cache used by Run when running in Node. By default, it caches 10MB of data in memory. You can increase this by setting run.cache.sizeMB. The local in-memory cache is useful and compliments a persisted cache.
Increase the state cache size
run.cache.sizeMB = 1000
Increase Node Memory Limits
You may need to increase Node's memory limits if you have many jigs loaded or cached at once. If you launch your server by running a node process, you can increase its memory via: node --max-old-space-size=8192.
Persist the Cache
It is a very good idea to persist your cache in a database so your backend never needs to load the same jig twice. It is even more important when using multiple servers because you can share this database. Simply implement the Cache API.
Run will automatically call get on your Cache implementation when values are needed, and set when values are ready to be cached. Cached values will never change for a given key. Just implement these two methods to load and save the cache into your preferred key-value database: Redis, Firestore, DynamoDB, etc.
To the right is an example of persisting in Google Firestore. You may want to modify it so that it also checks an in-memory local cache first and then goes to Firestore if that misses.
Save state into a Firestore collection
class Firestore {
async get(key) {
// Firestore does not allow forward slashes in keys
key = key.split('://').join('-')
const entry = await db.collection('state').doc(key).get()
if (entry.exists) return entry.data()
}
async set(key, value) {
// Firestore does not allow forward slashes in keys
key = key.split('://').join('-')
return db.collection('state').doc(key).set(value)
}
}
const run = new Run({ cache: new Firestore() })
Avoid Race Conditions
If you are seeing server errors about Missing inputs or txn-mempool-conflict, it is likely that you have encountered a race condition. Two pieces of code are likely attempting to update the same jig at the same time, and are attemping to spend the same Bitcoin outputs. The network can only accept one. To solve this, here are a few recommendations:
Load jigs once at the start: If you have jigs you are reused across many request handlers, like a class that you mint from, load those jigs or code once upon starting your server rather than each time they are used. This will let Run track the output correctly between asyncronous calls, and successive updates on that object will be performed in the right order.
Separate server, separate purses: Every Run instance has its own internal update queue, so it is unlikely that the purse will be double-spent on a single server. But on multiple servers, if a purse key is shared, then it is very likely that double-spend errors will occur. To avoid this, use different purse keys on different servers.
Sync after every update: Call await jig.sync() on jigs after you call a method to be sure that the updates were applied before continuing on.
Serialize shared updates: If a request handler makes updates to multiple jigs, and these updates can conflict if interleaved across multiple users, then consider serializing these updates. Within a single request handler, you can bundle all jig updates together in a batch, so that the operation is all-or-nothing. Across multiple request handlers, you can put updates into a task queue like p-limit.
Add retry logic: If the above changes do not fix a race condition, consider adding retry logic. Run will retry network calls, but race conditions require manual handling by your server.
Load Only Safe Code
Even with sandboxing, arbitrary code has the potential for infinite loops and infinite memory attacks that can take down your server. Avoid running code that you do not trust. You should only add transaction IDs to your trusted set that you know are safe. Also, it is generally not recommended to call run.inventory.sync() on servers, because that will load UTXOs you receive from others.
Debugging
Sometimes you will be faced with errors you don't understand. Don't fret! Here are some tips:
Grab the latest Run SDK from NPM or run.network
Use Node 10+ or a modern web browser like Chrome, Firefox, Edge, or Safari
Use the mockchain to check that it is not a connection or server-side error
Enable Run's internal logs by passing { logger: console } into the Run constructor
Writing Unit Tests
It is always a good idea to write unit tests for your jigs. We recommend using a framework like mocha or jest, and running your tests using the mockchain. The mockchain will be faster and will isolate your Jig logic from any network errors.
Getting Help
Join the Run Nation Discord Server, or join RunCraft's telegram group. You can also open an issue on one of RunCraft's github repos.
We are happy to help you debug and fix any issues you encounter. If you can, see if you can reproduce the issue in the browser's web console.