Skip to content
Official repo for World's Fair
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
lib
scripts
src
test
BENEFICIARIES.json
README.md
WHITEPAPER.md
index.js
package-lock.json
package.json

README.md

World's Fair Developer Docs

Welcome

This documentation is meant to help developers implement novel/niche applications of World's Fair, and, in general, support the emergence of decentralized mechanisms to fund art and open source technology.

For live chat with other dapp developers and additional help/resources/networking please consider joining our #buidl channel on the World's Fair Discord server.

The API is actually three different APIs, mirroring the 3-layer structure of World's Fair:

  • Web API: Get json data from our server. Our backend is continually reading the most recent World's Fair data from the blockchain, and processing/organising it into a more easily queryable format, as well as integrating with meta data from our frontend. We also provide routes to GET/POST cryptographically signed threads and comments, stored on IPFS and linked to a user's identity on the blockchain.

  • JavaScript API: Read/write World's Fair data on the blockchain using web3 from browser or Node backend.

  • Solidity API: The actual source code of the World's Fair contract, with lots of comments so you can understand how it works and how to integrate your contract.

Self Funding Open Source

Wouldn't it be great (for both developers and users) if make it awesome and make everything free was a valid monetization strategy? That's the central theoretical principle of a value-consensus economy!

Each asset on World's Fair is seeking a state of agreement about its monetary value. In the process of reaching value-consensus, funding naturally flows to assets in proportion to the perceived value of what it is they represent. Open source creates an incredible amount of value already. World's Fair is simply a mechanism by which to negotiate what that value is in monetary terms. It's as if value were brought into existence by the act of measuring it.

We'd like to be able to accomplish this in a secure, manipulation-resistant, and socially responsible way. Nobody quite knows what kind of applications are, or may become, possible. If you can contribute something valuable (ha) consider creating an asset for your git repo so people can support your work and keep growing the ecosystem.

For ideas and discussion about ongoing projects, check out the community forum. Happy coding ✌

Web API

JavaScript API

Solidity API / Contract Code


Web API

The Web API provides json data which has been processed and packaged by our backend server into a queryable database (this is a subset of the api used by the official front-end). The endpoint for testnet data is https://api-rinkeby.worldsfair.io while mainnet data can accessed at https://api.worldsfair.io. You don't need an API key. Certain rate limits are enforced, so do avoid making lots of requests in rapid succession from the same IP.

Much of the data in the Web API is the same as can be read directly from the blockchain using the JavaScript API, but also includes certain public meta data (like watching and following figures for assets) and various high-level data and functions for the global state of World's Fair.

In addition to the various GET routes, there are two routes that allow you to POST signed data: POST /thread and POST /comment are provided to allow the implementation of third-party community clients (the Community Section functions much like reddit, check it out).

GET World's Fair Data

GET /global Global Stats

const res = await axios.get(`${API}/global`);

Returns top-level statistics for the World's Fair. Data is updated every few minutes.

Parameters

None

Returns Object

  • total: String, total valuation, sum of all profits
  • figures: Object
    • creators: String, wei paid to creators in icos and royalties
    • shareholders: String, wei paid to investors from trading
    • beneficiaries: String, wei paid to charity
  • content: Object, number of assets tagged as each type
    • video / film: Number
    • audio / music: Number
    • visual / design: Number
    • words / language: Number
    • podcast / spoken: Number
    • source / code: Number
    • project / tech: Number
    • peace / love: Number

GET /search Search Assets and Accounts

const query = 'reggae dub'; // match account username/address or asset title/tags/ipfs
const res = await axios.get(`${API}/search?q=${query}`);

Given a search term, returns up to 8 objects representing assets and accounts. Assets are matched by their title and tags, while accounts are matched by username. When the query matches an account, results may include assets that were created by that account. You can also fetch an account by querying its linked Ethereum address. If the query is an IPFS hash, server returns matching assets. The server will reject queries exceeding 100 chars.

Parameters

  • q: required, search query

Returns [Object]

  • Object account
    • type: String, account
    • username: String, username of the account
    • address: String, account's linked address
  • Object asset
    • type: String, asset
    • signature: String, signature of asset
    • title: String, title of asset
    • owner: String, username of author account
    • valuation: String, current valuation of asset in wei
    • time: Number, timestamp of block when asset was published

GET /asset Assets

// Default front-page asset feed (no params)
const q0 = '';

// Specific asset (provide asset signature)
const q1 = '?signature=0x6a03e1da5ad3563f6afb74f7a1f53f7edaf9e5d6';

// Most recent assets created by a specific account (provide username)
const q2 = '?owner=picklerick&sort=new';

// Exclude assets that don't have proper meta data (ie title and tags)
const q3 = '?mode=strict';

// Assets matching any of the provided tags
const tags = ['music / audio', 'reggae', 'dub'];
const q4 = `?match=any&tags=${encodeURI(JSON.stringify(tags))}`;

const res = await axios.get(`${API}/asset${/*your_query*/}`);

Returns assets (in batches of 25) matching various search parameters. This route should allow developers to implement asset browser clients in environments that don't yet have a lot of support for blockchain-enabled actions (eg mobile). The current default batch size for results is 25. The examples above are just a few cases—you can freely combine/omit query components as needed to suit your application.

Parameters

  • signature: optional, get specific asset
  • owner: optional, get assets created by account
  • sort: optional, order results by recent (recently created), ico (upcoming ico), watching (number of accounts watching), investment (highest valuation first), or investment24h (most profits generated in last 24 hours)
  • match: optional, return assets matching any or all tags
  • tags: optional, uri-encoded array of up to 8 tags to match
  • mode: optional, if strict exclude assets without proper meta
  • skip: optional, number of results to skip (for pagination)

Returns Object

  • endOfList: Boolean, true if returned results represent all matches for query
  • assets: [Object], array of asset objects
    • Object asset
      • title: String, asset title
      • description: String, asset description
      • descriptionHtml: String, rendered and sanitized description html
      • tags: [String], array of tags
      • embed: String, http platform (for content embedding)
      • signature: String, asset signature
      • ipfsHash: String, IPFS hash of content
      • http: String, canonical link to content
      • owner: String, username of author account
      • beneficiary: String, beneficiary Ethereum address
      • timePublished Number, unix time when asset was created
      • totalShares: Number, how many shares asset is tokenized into
      • pOwner: Number, percent or profits that owner receives as royalites
      • pBeneficiary: Number, percent of profits sent to beneficiary
      • beneficiaryName: String, human-readable name for known beneficiary addresses
      • contributed: String, amount asset has contributed to beneficiary to date
      • limitConstant: Number, Appreciation Limiting Constant (ALC) x 10000
      • icoOpen: Number, unix time when ICO opens (shares become tradeable)
      • icoCompleted: Number, unix time when asset reached Minimum Valuation target (undefined for ongoing ICOs)
      • initialPrice: String, ICO price for one share in wei
      • watching: Number, number of accounts watching this asset
      • ipfsValid: Boolean, true if ipfsHash is a valid IPFS multihash
      • valuation: String, asset's total valuation (sum of all profits in wei)
      • v24h: String, sum of profits in last 24 hours in wei

GET /shareholder Asset Shareholders

const res = await axios.get(`${API}/shareholder?asset=0x6a03e1da5ad3563f6afb74f7a1f53f7edaf9e5d6`);

Given an asset signature, returns all the current (meaning that they own at least one share) shareholders for that asset.

Parameters

  • asset: required, signature of asset you want to fetch shareholders for

Returns [Object]

  • Object shareholder
    • account: String, username of shareholder account
    • shares: Number, how many shares currently owned by shareholder
    • ask: String, shareholders current asking price in wei
    • weiIn: String, total wei account has ever spent on shares of this asset
    • weiOut: String, total wei account has ever received as revenue from selling shares of this asset
    • lastBuy: Number, unix time when shareholder last acquired shares
    • address: String, Ethereum address currently linked to shareholder account

GET /asset/exists Check If Asset Exists

const res = await axios.get(`${API}/asset/exists?username=picklerick&ipfs=QmWn3yBeu8Akm6rwC1zNvHTmvpCjfHchmBQs15YTMawu9S`);

Given an account username and an IPFS multihash, returns true if such an asset exists (an asset is uniquely defined by these two pieces of data).

Parameters

  • username: required, username of account
  • ipfs: required, ipfs multihash of content

Returns Boolean

GET /asset/transactions Asset Transactions

const res = await axios.get(`${API}/asset/transactions?asset=0x6a03e1da5ad3563f6afb74f7a1f53f7edaf9e5d6`);

Given an asset signature, returns the transaction history for that asset.

Parameters

  • asset: required, signature of the asset you wish to fetch transactions for

Returns [Object]

  • Object transaction
    • seller: String, username of account that sold shares
    • buyer: String, username of account that bought shares
    • shares: Number, how many shares
    • buyPrice: String, share price in wei buyer paid
    • newAsk: String, new share price in wei buyer is asking
    • time: Number, unix time of transaction
    • transactionHash: String, hash of transaction
    • transactionIndex: Number, index of transaction in block
    • blockNumber: Number, block number transaction was included in

GET /beneficiary/list List of Beneficiaries

const res = await axios.get(`${API}/beneficiary/list?set=whitelisted&sort=cumulative`);

Returns a list of beneficiaries. Each time someone creates an asset with a new beneficiary, our server reads that data off the blockchain and creates a model for it if it doesn't already exist. Admins manually review these addresses to see (basically by Googling the address) if they are linked to some known charitable entity. If so, we add the name, domain, and link properties. If you want to only fetch results these human-reviewed results, pass the set parameter as whitelisted. This beneficiary list is ever-growing and anyone is welcome to use it as a general purpose source of Ethereum donation addresses, even if your application has nothing to do with World's Fair. Results are returned in batches of 50.

The full json file of known beneficiaries is also available on GitHub.

Parameters

  • set: optional, pass whitelisted to only get admin-reviewed results
  • sort: optional, can be cumulative (highest total contributions first) or current (highest balance ready for payout first)
  • skip: optional, number of results to skip

Returns Object

  • endOfList: Boolean, true if returned results represent all matches for query
  • beneficiaries: [Object], array of beneficiary objects
    • Object: beneficiary
      • address: String, Ethereum address of beneficiary
      • supporting: Number, how many assets named this address as beneficiary
      • domain: String, link to main website of beneficiary
      • name: String, human readable name of beneficiary, if known
      • link: String, link to webpage where donation address was sourced
      • current: String, amount of wei ready for payout
      • cumulative: String, cumulative amount of wei ever raised
      • updated: Number, when the beneficiary's info was added or last updated

GET /beneficiary/data Beneficiary Data

const res = await axios.get(`${API}/beneficiary/data?beneficiary=0xc7FF9555C8df69f923C0A72EDdbfd6a079d6D9B3`);

Given an Ethereum address, returns data about contributions received and possibly meta data about the charitable entity associated with the address, if known. Server responds 404 if the provided address has never been named as beneficiary by any asset.

Parameters

  • beneficiary: required, beneficiary Ethereum address

Returns Object

  • name: String, human readable name of beneficiary, if known
  • link: String, link to webpage where donation address was sourced
  • domain: String, link to main website of beneficiary
  • known: Boolean, true if meta data has been manually added by admins
  • supporting: Number, how many assets named this address as beneficiary
  • current: String, amount of wei ready for payout
  • cumulative: String, cumulative amount of wei ever raised

GET /beneficiary/transactions Beneficiary Credits & Payouts

const res = await axios.get(`${API}/beneficiary/transactions?beneficiary=0xc7FF9555C8df69f923C0A72EDdbfd6a079d6D9B3`);

Given a beneficiary Ethereum address, return events in which this beneficiary recieved ether as a result of trading shares, in addition to events in which the payout was triggered (causing the ether to actually be sent from the World's Fair contract). In the case of such payouts, if it was triggered by an Ethereum address linked to an account, the account name is triggeredBy (otherwise it's an empty string). Results are returned in batches of 50.

Parameters

  • beneficiary: required, beneficiary Ethereum address
  • before: optional, unix time for which to omit results that are timestamped later
  • skip: optional, number of results to skip

Returns Object

  • endOfList: Boolean, true if returned results represent all matches for query
  • events: [Object], array of beneficiary events
    • Object: credit event
      • event: String, credit
      • asset: String, signature of asset in transaction resulting in credit
      • amount: String, amount credited in wei
      • beneficiary: String, Ethereum address of beneficiary
      • time: Number, unix time of credit
    • Object: payout event
      • event: String, payout
      • triggeredBy: String, username of account (if any) linked to address that triggered payout
      • amount: String, amount of wei paid to beneficiary
      • beneficiary: String, recipient Ethereum address
      • time: Number, unix time of payout
      • number: Number, index of payout

GET /account/available Check Username Availability

const res = await axios.get(`${API}/account/available?username=PiCkLeRiCk`);
console.log(res.data); // false

Given a username, returns true if that account is able to be registered. It's worth pointing out that just because an account does not exist, this does not neccessarily mean that it's available for registration. That's because, in an effort to provide good UX and make scams more difficult, World's Fair server considers all case variants of a username "taken" when an account is created. It's certainly possible to register case-variants (because the contract only cares about the byte representation of the username) but it's not recommended because our server will refuse to index them when synchronizing with new data from the blockchain. You are encouraged to follow this convention when designing your own UX to maximize cross-compatibility between World's Fair clients.

Parameters

  • username: required, username you want to check availability for

Returns Boolean

GET /account/portfolio User's Public Portfolio

const res = await axios.get(`${API}/account/portfolio?username=picklerick`);

Given a username, returns data about all the asset shares currently held by that account, in additional to stats about portfolio totals.

Parameters

  • username: required, username of account
  • sort: optional, order results by

Returns Object

  • stats
    • consensusValue: String, total consensus value of holdings
    • totalInvested: String, total wei invested in asset shares
    • change24h: String, total change in consensus value of holdings in the last 24 hours
    • charity: String, total wei contributed to beneficiaries from account's holdings
    • netProfit: String, amount in wei that total revenue exceeds total investment for all holdings (can be negative)
  • holdings: [Object], array of holdings
    • Object portfolio item
      • ask: String, account's current asking price in wei
      • asset: String, signature of asset
      • title: String, title of asset
      • shares: Number, how many shares account owns
      • weiIn: String, total amount (in wei) account has ever spent on shares of this asset
      • weiOut: String, total amount (in wei) account has ever recieved in as revenue from sale of shares
      • totalShares: Number, total number of shares asset is divided into
      • initialPrice: String, ICO price of asset in wei
      • limitConstant: Number, asset's Appreciation Limiting Constant (ALC) x 10000
      • pOwner: Number, percent of profits (in addition to ICO revenue) sent to author/owner
      • pBeneficiary: Number, percent of profits sent to beneficiary address (from account's investment)
      • toBeneficiary: String, amount of wei sent to beneficiary (from account's investment)
      • toOwner: String, amount of wei sent to owner (from account's investment)
      • profit: String, amount in wei that revenue exceeds investment (can be negative)
      • lastBuy: Number, unix time when account most recently bought shares in this asset
      • lastSell: Number, unix time when account most recently sold shares
      • v24h: String, amount of wei consensus value of shares has increased in the last 24 hours
      • percentStake: Number, approximate percent of asset's shares owner by account
      • consensusValue: String, consensus value of shares in wei
      • contentType: String, content type of asset

GET /account/data User's Public Profile

const res = await axios.get(`${API}/account/data?username=picklerick`);

Given account username, returns public bio information for account.

Parameters

  • username: required, account username

Returns Object

  • address: String, account's linked address
  • followers: Number, how may accounts are following this account
  • created: Number, uinx time account was created

GET /thread Threads

// Get default front-page threads
const threads = await axios.get(`${API}/thread`);

// Get only threads by picklerick
const authorThreads = await axios.get(`${API}/thread?author=picklerick`);

// Get one specific thread by its IPFS hash
const thread = await axios.get(`${API}/thread?hash=Qmeg8qRQc4ce6QtRi4zy6tSvQqEW7sPWkh1qVwRMJKywK9`);

// Get older threads (useful for pagination)
const before = Math.floor(Date.now() / 1000) - (60 * 60 * 24); // Unix time 24 hours ago
const olderThreads = axios.get(`${API}/thread?before=${before}`);

Returns community threads. To fetch the default threads (those found on the front page of the Community section) just hit the url with no parameters. If you just need to fetch a specific thread, pass that thread's hash in the query. If you want to fetch all the threads created by a certain account, pass that account's username as author. Data is returned in batches of 50 threads. The before parameter can be used to paginate results by setting it equal to the oldest thread that your client has already fetched on the previous request. as tells the server to mark the upvoted and downvoted fields with the appropriate value, defaulting to false.

Parameters

  • hash: optional, hash of specific thread to fetch
  • author: optional, fetch all threads by certain account (pass username)
  • before: optional, unix time for which to omit results that are timestamped later
  • as: optional, username of account making request (ie allows server to set upvoted and downvoted for signed in user)

Returns Object

  • endOfList: Boolean, true if returned results represent all matches for query
  • threads: [Object], array of thread objects
    • Object: thread
      • hash: String, IPFS hash of signed thread data (use as GUID)
      • author: String, username of account linked to address which signed data
      • address: String, Ethereum address that signed data
      • title: String, thread title
      • content: String, selftext of thread, if any
      • html: String, rendered and sanitized html (threads support markdown rendering)
      • announcement: Boolean, if announcement thread
      • sticky: Boolean, if sticky thread
      • upvoted: Boolean, if username passed in as parameter has upvoted thread
      • downvoted: Boolean, if username passed in as parameter has downvoted thread
      • points: Number, net upvotes/downvotes received
      • comments: Number, how many comments on this thread
      • time: Number, unix time when thread was posted

GET /comment Comments

// Get a thread's comments
const threadComments = axios.get(`${API}/comment?thread=Qmeg8qRQc4ce6QtRi4zy6tSvQqEW7sPWkh1qVwRMJKywK9`);

// Get an asset's comments
const assetComments = axios.get(`${API}/comment?asset=0x6a03e1da5ad3563f6afb74f7a1f53f7edaf9e5d6`);

// Get an account's comments with `as` specified so server can return upvoted/downvoted flags
const authorComments = axios.get(`${API}/comment?author=picklerick&as=morty`);

Returns comments. sort parameter is required and may be set to new or top. It required that you pass either asset (signature of an asset), thread (hash of a thread) or author (username of an account). as tells the server to mark the upvoted and downvoted fields with the appropriate value, defaulting to false.

Parameters

  • sort: optional, can be top (default, most points first) or new (most recent)
  • asset: required (only if thread or author not specified), signature of asset to fetch comments for
  • thread: required (only if asset or author not specified), hash of thread to fetch comments for
  • author: required (only if asset or thread not specified), username of account to fetch comments for
  • as: optional, username of account making request (ie allows server to set upvoted and downvoted for signed in user)

Returns [Object]

  • Object comment
    • asset OR thread: String, signature of asset (if asset comment) OR hash of thread (if thread comment) to which comment belongs
    • parent: String, hash of parent comment (empty string indicates top level comment)
    • content: String, content of comment
    • html: String, rendered and sanitized html (comments support markdown rendering)
    • sticky: Boolean, if sticky comment
    • upvoted: Boolean, if username passed in as has upvoted comment
    • downvoted: Boolean, if username passed in as has downvoted comment
    • hash: String, IPFS hash of signed comment data (use as GUID)
    • author: String, username of account linked to address which signed data
    • address: String, Ethereum address that signed data
    • points: Number, net upvotes/downvotes received
    • time: Number, unix time when comment was posted

POST Signed Community Data

You might notice that you get a MetaMask popup asking to sign data using your Ethereum address whenever you create a new thread or post a comment on World's Fair. Doing it this way means your client does not have to authenticate with the our server to POST comments and threads: our backend simply calls directory(decoded_address) on the contract to find the account linked to that Ethereum address (which is saved at the author). Prior to sending the signed data, you must upload the signed comment and its signature to IPFS. The multihash that you obtain is used as the GUID of the comment or thread, which is also recorded by the server.

Importantly, the signed data includes a parent field (the IPFS hash of the parent comment, if not top-level). This gives comment trees a property that is almost blockchain-like in the sense that the hash of every comment in a thread depends on the hash of every one its ancestors (the hash of a comment depends on the hash of its parent, which in turn depends on the hash of its parent, up until the top comment is reached). This makes comment trees extremely resistant to manipulation because it's easy to compute the hash of each comment to check if it matches the hash encoded in the data of all the child comments.

So the process of posting data is bascically this:

  1. A comment or thread is signed by the user's linked Ethereum address using MetaMask.
  2. The signed data (which includes the IPFS hash of any parent comment) and its resulting sig are added to IPFS to obtain an IPFS of that data hash.
  3. This data is posted to the server, which infers the author of the thread or comment by decoding which address was used to sign it and then looking at the blockchain to find the linked World's Fair account. If the address is not linked to any account, or if the reported IPFS hash does not actually match the hash of the data, the server will reject the request.

Our server is one gateway for this kind of open and decentralized commenting protocol. The fact that you don't have to authenticate with our server is a great illustration of how powerful a global, distributed record like the Ethereum blockchain really is... especially when data can be cryptographically linked to a real identity. Since World's Fair accounts are associated with certain types of state that are fundamentally scare (ie ether balances, asset shares) they can, to a certain extent, act as a bridge between the Ethereum blockchain and real humans—and the applications are not at all limited to World's Fair. If you wanted to build a dapp that allowed people to "sign in with World's Fair" (or otherwise authorize certain functions) it would be trivially easy to do so by prompting the user to sign { foo: 'bar' } and then sending that data to another party who would simply decode the signing address and look at the blockchain to see what account it was linked to. Being an "identity provider" is nothing new, obviously, but the advantage gained by having the blockchain act as the source of truth (rather than Google/Facebook/whatever) is that nobody not even World's Fair is in any sort of privileged position. Ultimately, it makes more sense to think of "ownership" of identity-linked data structures on the blockchain being determined solely by a private key, not by the contract in which the data happens to be defined. The bottom line is this: if you want to use World's Fair accounts for your own purposes, go for it, because we can't stop you anyway.

In the examples below, it's assumed that your dapp is using MetaMask and the World's Fair npm module:

npm install --save worldsfair

In your js, pass the web3 that you got from MetaMask to the World's Fair constructor and set the appropriate network option.

import WorldsFair from 'worldsfair';
const wf = new WorldsFair(web3, { network: 'rinkeby' });

Now you can use the helper methods addToIPFS(), signComment(), and signThread() which makes the whole process quite straightforward. Check out the following examples for posting a comment or thread, respectively:

POST /comment Post Comment

// These examples show how to post a comment on a thread or on an asset:

let threadComment;
let assetComment;

// First we need to cryptographically sign the comment data with
// user's linked Ethereum address. MetaMask will prompt this action
// with a popup to be signed by the currently selected address. There
// are two types of comments: *asset comments* and *thread comments*.
// The only distinction is whether you pass the `asset` or `thread`
// option. The examples below illustrate each case. The last thing to
// know is that for top-level comments (replying directely to the thread
// or asset instead of another comment) you should leave `parent` undefined.

try {
  
  // Comment on a thread, replying to another content
  threadComment = await wf.signComment({
    content: "This is a comment", // Content limited to 5000 chars
    thread: 'QmRvyEWWnpERD9xEkAMiCeGZM6Pvj2hYSerAqh3WwKwrVe', // IPFS hash of thread
    parent: 'QmZQ6KRJxxAhYohPJatY3NtL4GkhYFDbkCRDhn1M8XZL9b' // IPFS hash of parent comment
  });

} catch () {
  // Most likely user rejected transaction signature
}

// *OR*

try {

  // Top-level comment on an asset (no parent)
  await wf.signComment({
    content: "This is a comment",
    asset: '0x075b2dd5bae7005105135a9a82429b9b9365cad8' // Signature of asset
  });

} catch () {
  // Most likely user rejected transaction signature
}

// Let's add the first example (the thread comment) to IPFS.
// By default, World's Fair uses the public Infura IPFS gateway.
// You are free to specify your own by passing an options object
// like `{ gateway, port, protocol }` as the second parameter
// for the `addToIPFS()` helper function. For this example,
// we'll stick with the defaults.

let hash;

try {

  // Uploading the data will return an IPFS hash. You don't
  // need to keep this hash (because our server computes
  // it again anyway, but maybe you need it for something...)
  hash = await WorldsFair.addToIPFS(threadComment);

} catch () {
  // Possibly an issue with the IPFS gateway
}

// Now that the data is on IPFS, let's post it to the
// World's Fair server... no prior authentication required!

try {

  // Response returns saved comment object
  const res = await axios.post(`${API}/comment`, threadComment);

} catch () {
  // See 'Restrictions' to troubleshoot failed requests
}

Data

  • content: required, text of the comment
  • thread: optional, IPFS hash of thread
  • asset: optional, signature of asset
  • parent: optional, IPFS hash of parent comment

Restrictions

  • content must not be empty
  • content length must be less than or equal to 5000 chars
  • asset OR thread must be specified (but not both)
  • asset, if provided, must be signature of an existing asset
  • thread, if provided, must map to an existing thread
  • parent, if provided, must map to an existing comment
  • address used to sign data must be currently linked to a World's Fair account

POST /thread Post Thread

// Posting a thread is substantially the same as posting a comment.
// See the above comment examples for elaboration on the finer points.

// Sign the data
let thread;
try {
  thread = await wf.signThread({
    title: 'Title of the thread' // Limited to 160 chars
    content: "Optional self text of the thread" // Limited to 40000 chars
  });
} catch () {
  // Most likely user rejected transaction signature
}

// Add to IPFS
let hash;
try {
  hash = await WorldsFair.addToIPFS(thread);
} catch () {
  // Possibly an issue with the IPFS gateway
}

// POST to server
try {
  const res = await axios.post(`${API}/comment`, threadComment);
} catch () {
  // See 'Restrictions' to troubleshoot failed requests
}

Data

  • title: required, title of the thread
  • content: required, self text

Restrictions

  • title must not be empty
  • title length must be less than or equal to 160 chars
  • content length must be less than or equal to 40000 chars
  • address used to sign data must be currently linked to a World's Fair account

JavaScript API

Getting Started

The Easy Way

npm install --save worldsfair

The official World's Fair npm module is a just a lightweight wrapper around a vanilla web3 contract instance that comes bundled with the contract address, interface, and some static utility methods.

Pass the ethereum provider object injected by MetaMask as the first parameter in the constructor. Then set the network option to either 'rinkeby' (for the version of the contract deployed to the Rinkeby Test Network) or 'main' (for the real contract on the mainnet). Check it out:

import WorldsFair from 'worldsfair';

// Pass provider object from MetaMask as first parameter
// The network option can be 'rinkeby' or 'main'
const wf = new WorldsFair(window.ethereum, { network: 'rinkeby' });
let contract;

try {
	contract = await wf.getContractInstance(); // Get a web3 contract instance
} catch (err) {
	// something went wrong
}

// Now you can do stuff with the contract object

getContractInstance() returns a web3 contract object that you can use to send transactions or make calls to read data as you normally would. That's it.

The Slightly Less Easy Way

If you don't want to use the npm module, you can get the contract object directly from web3, but you'll need to supply the contract address and interface, which can be found here.

import web3 from 'web3';
import abi from './interface.json'; // Your path may vary

let contract;

try {
	contract = await new web3.eth.Contract(
		abi, // Interface
		'0x731C54d14d853af7f6CB587c680Efc1db11a3757' // Address
	);
} catch (err) {
	// something went wrong
}

// Now you can do stuff with the contract object

Writing World's Fair Data

The following functions allow you to write data to the World's Fair contract. You might notice that in the examples the gas property is not set when sending the transaction. This is because it is assumed that you are using the web3 object injected by MetaMask, which calculates max gas automatically. For clarity, we're also assuming that you already got accounts by calling web3.eth.getAccounts() somewhere in scope.

Create an Account

contract.methods.createAccount(
	web3.utils.asciiToHex('picklerick', 20), // username must be 3-20 chars, alphanum + dash/underscore
	accounts[1] // Recovery address is passed as string
).send({
	from: accounts[0] // Address that sends the transaction will be user's linked address
});

createAccount() creates a World's Fair account. The address used to send the transaction will become the account's linked address. The first parameter is the username—you'll want to use the asciiToHex() function that comes with web3 to convert this value to hex so it can be understood by the contract, which is expecting bytes20. It is possible to register usernames less than 3 characters in length, but they won't be indexed by the World's Fair server. More than 20 characters will be truncated.

The second parameter is the recovery address. This is optional, and you can skip it by passing the zero address. View Contract Code

Parameters

  • bytes20 username : required
  • address recovery : optional, pass zero address to skip

Restrictions

  • username must not be empty string
  • username must not match an existing account
  • recovery must not be the same as sender address
  • sender must never have used before

Create an Asset

contract.methods.createAsset(
	'QmWn3yBeu8Akm6rwC1zNvHTmvpCjfHchmBQs15YTMawu9S', // IPFS multihash
	'worldsfair.io', // Canonical link to content
	31416, // Total number of shares
	web3.utils.toWei('0.01', 'ether'), // ICO share price (in wei)
	'0xc7FF9555C8df69f923C0A72EDdbfd6a079d6D9B3', // Beneficiary address
	20, // Owner receives 20% royalties on profits
	5, // Beneficiary receives 5% royalties on profits
	(5 * 10000), // Appreciation limiting constant is 5 (must always multiply by ten thousand)
	Math.floor(Date.now() / 1000) + (60 * 60 * 24) // ICO opens in 24 hours
).send({
	from: accounts[0] // Transaction must be sent from author's linked address
});

createAsset() allows a user to create a new asset. The address used to send the transaction must be linked to a World's Fair account, which will be the owner/author of the new asset. If you name a beneficiary address, pBeneficiary may not be equal to zero. Similarly, if pBeneficiary is greater than zero, you must name a beneficiary address. Since pOwner and pBeneficiary represent percent values, their sum cannot exceed 100. View Contract Code

Parameters

  • string ipfsHash : required
  • string http : optional, can be empty string
  • uint totalShares : required, min 1, max 1 billion
  • uint initialPrice : required, min 0.000001 ETH, max 100 million ETH
  • address beneficiary : optional, pass zero address to skip
  • uint pOwner : optional, min 0, max 100
  • uint pBeneficiary : optional, min 0, max 100
  • uint icoStart : optional, pass zero for ICO to open immediately

Restrictions

  • ipfsHash must have never previously been published by this account
  • ipfsHash must not be empty string
  • totalShares must be greater than zero
  • totalShares must be less than MAX_SHARES
  • initialPrice must be greater than or equal to MIN_SHARE_PRICE
  • initialPrice must be less than or equal to MAX_SHARE_PRICE
  • sum of pOwner and pBeneficiary must be less than or equal to 100
  • limitConstant must be zero or greater than or equal to LIMIT_RESOLUTION
  • beneficiary must not be the same as sender address
  • if beneficiary is not zero address, pBeneficiary must be greater than zero
  • if pBeneficiary is greater than zero, beneficiary must not be zero address
  • sender address must be linked to an account

Buy Shares

import bigInt from 'big-integer';

// You must send enough ether to cover the cost of
// of the order. Ether held in the buyers balance
// will be applied to the cost of the order, but for
// this example let's assume the buyer's internal balance
// is zero. To avoid precision errors, please don't carry
// out calculations involving amounts of wei using JavaScript
// numbers. It's recommended that you use big-integer in such cases.

// Calculate cost of order
const sharePrice = web3.utils.toWei('0.01', 'ether');
const sharesOrdered = 1500;
const totalCost = bigInt(sharePrice).multiply(sharesOrdered).toString();

contract.methods.buyShares(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149', // Signature of the asset
  web3.utils.asciiToHex('morty', 20), // Username of shareholder selling shares in bytes20
  sharesOrdered, // Number of shares to buy from this seller
  sharePrice, // Seller's asking price (in wei)
  web3.utils.toWei('0.025', 'ether'), // Buyer's new asking price (in wei)
  false // Buy whatever shares seller has left, if less than sharesOrdered
).send({
	from: accounts[0], // Transaction must be sent from buyer's linked address
	value: totalCost // Ether to pay for the shares
});

buyShares() allows a user to buy shares of an asset. Make sure to send enough ether with the transaction to cover the cost of the shares, as shown in the above example. Any extra ether that you send will be credited to the buyer's account balance. You must specify the username of the shareholder from whom you wish to buy shares. If the ICO is still ongoing (which is to say that the author has not sold all of their shares) attempts to buy shares from any user other than the author will fail.

Transactions can also fail because newAsk or sharePrice fall outside bounds set by an asset's Appreciation Limiting Constant. The last parameter is a boolean to enable 'strict' mode, meaning that the transaction should fail if the seller doesn't have enough shares to match sharesOrdered. Otherwise, the contract will complete the transaction by falling back to however many shares the seller has left. View Contract Code

Parameters payable

  • bytes20 signature : required
  • bytes20 sellerUsername : required
  • uint sharesOrdered : required
  • uint sharePrice : required
  • uint newAsk : required
  • bool strict : required

Restrictions

  • sender address must be linked to an account
  • current blocktime must be greater than or equal to than asset's icoStart
  • sender address must not be linked to sellerUsername
  • sharesOrdered must be greater than zero
  • sender address must not be linked to account that created the asset
  • if strict is true, sharesOrdered must be less than or equal to number of shares owned by shareholder matching sellerUsername at the time that the transaction is mined
  • sharePrice must equal asking price of shareholder matching sellerUsername
  • newAsk must be greater than or equal to asset's initialPrice
  • if buyer currently owns shares of this asser newAsk cannot be higher than current asking price
  • if ico is not yet complete sellerUsername must be asset author/owner
  • for asset's with an Appreciation Limiting Constant, newAsk must be less than or equal to sharePrice multiplied by the ALC and sharePrice must be less than or equal to asset's valuation per share (see contract code)
  • for asset's without an Appreciation Limiting Constant, newAsk must be less than or equal to MAX_SHARE_PRICE
  • value of transaction added to internal balance of account linked to sender address must less greater than or equal to sharesOrdered multiplied by sharePrice

Lower Asking Price

contract.methods.lowerAskingPrice(
	'0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149', // Signature of the asset
	web3.utils.toWei('0.02', 'ether') // New asking price (in wei)
).send({
	from: accounts[0] // Transaction must be sent from shareholder's linked address
});

lowerAskingPrice() allows a user to lower the asking price for shares that they already hold. The asking price for shares of a given asset can never be lower than the ICO price. View Contract Code

Parameters

  • bytes20 signature : required
  • uint newAsk : required

Restrictions

  • account linked to sender address must match asset's owner
  • shareholder linked to sender address must own at least 1 share in asset
  • newAsk must be greater than or equal to asset's initialPrice
  • newAsk must be less than existing asking price

Set Canonical Link

contract.methods.setCanonicalHttp(
	'0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149', // Signature of the asset
	'rinkeby.worldsfair.io' // New canonical link
).send({
	from: accounts[0] // Transaction must be sent from author's linked address
});

setCanonicalHttp() allows the owner/author to change the canonical link of their already-published asset. There is no limit (other than gas cost) on the length of the string. It's recommended that you do not include the protocol in the URL. View Contract Code

Parameters

  • bytes20 signature : required
  • string http : optional, can be empty string

Restrictions

  • account linked to sender address must match asset's owner

Change Linked Address

contract.methods.changeLinkedAddress(
	web3.utils.asciiToHex('picklerick', 20), // Username of account in bytes20
	accounts[2] // New linked address
).send({
	from: accounts[0] // Transaction must be sent from account's current linked address
});

changeLinkedAddress() allows a user to change the linked address of their account. View Contract Code

Parameters

  • bytes20 username : required
  • address newOwner : required

Restrictions

  • sender address must be account owner
  • newOwner must never have previously been linked to or used as the recovery address for any account
  • newOwner must not be zero address
  • newOwner must not be the same as existing linked address

Set Recovery Address

contract.methods.setRecoveryAddress(
	web3.utils.asciiToHex('picklerick', 20), // Username of account in bytes20
	accounts[3] // New recovery address
).send({
	from: accounts[1], // Transaction must be sent from account's linked address
});

setRecoveryAddress() allows a user to set a recovery address for their account. This function can only be called if the recovery address is not already set. The only way it can be set again is after calling recover(). This is a security feature to prevent an attacker who obtains the private to the linked address from simply using it to change the recovery address. View Contract Code

Parameters

  • bytes20 username : required
  • address recovery : required

Restrictions

  • sender address must be account owner
  • account must not already have a recovery address set
  • recovery parameter must not be zero address
  • recovery must never have previously been linked to or used as the recovery address for any account

Recover Account

contract.methods.recover(
	web3.utils.asciiToHex('picklerick', 20) // Username of account in bytes20
).send({
	from: accounts[1] // Transaction must be sent from account's recovery address
});

recover() allows a user to reassert ownership of their account in the event that they lose control of their linked address. When calling this function, the transaction must be sent from the recovery address—not the linked address. The recovery address becomes the new linked address and sets the recovery address is reset to zero, at which point the user may set a new recovery address. It's like a blockchain version of "forgot your password". View Contract Code

Parameters

  • bytes20 username : required

Restrictions

  • sender address must must match account's recovery address

Withdraw, Deposit, & Transfer Ether

Deposit Ether to Own Account

contract.methods.depositToSelf().send({
	from: accounts[1], // Transaction must be sent from account's linked address
	value: web3.utils.toWei('1', 'ether') // Deposit 1 ETH to picklerick's account
});

depositToSelf() allows a user to deposit ether to their World's Fair account balance from their linked address. There are no parameters for this function. The amount of ether you wish to deposit must be set on the value property when sending the transaction. View Contract Code

Parameters payable

None

Restrictions

  • sender address must be linked to an account
  • value of transaction must greater than zero

Deposit Ether to Other Account

contract.methods.depositToAccount(
	web3.utils.asciiToHex('morty', 20) // Username of recipient in bytes20
).send({
	from: accounts[9], // Can be deposited by any address (doesn't have to be account-linked)
	value: web3.utils.toWei('1', 'ether') // Deposit 1 ETH to morty's account
});

depositToAccount() allows any Ethereum address to deposit ether to the balance of any World's Fair account even if that address is not linked to any account. The one exception is that the linked address of an account cannot deposit ether into the account to which it's currently linked—use depositToSelf() instead (see above). The amount of ether you wish to deposit must be set on the value property when sending the transaction. View Contract Code

Parameters payable

  • bytes20 recipient : required

Restrictions

  • recipient must be an existing account
  • value of transaction must be greater than zero
  • recipient must not be the same as username of account linked to sender address

Withdraw Ether

contract.methods.withdrawFunds(
	web3.utils.toWei('0.5', 'ether') // Withdraw 0.5 ETH
).send({
	from: accounts[1] // Transaction must be sent from account's linked address
});

withdrawFunds() allows a user to withdraw ether from the balance of their World's Fair account. Obviously this function can only be called by the account's linked address. View Contract Code

Parameters

  • uint amount : required

Restrictions

  • amount must be greater than zero
  • amount must be less than or equal to internal balance of account linked to sender address

Transfer Ether Internally

contract.methods.transferFundsInternally(
	web3.utils.asciiToHex('morty', 20), // Username of recipient in bytes20
	web3.utils.toWei('0.15', 'ether') // Transfer 0.15 ETH
).send({
	from: accounts[1] // Transaction must be sent from sender account's linked address
});

transferFundsInternally() allows a user to make an internal transfer of ether directly from their World's Fair balance to the balance of another account. View Contract Code

Parameters

  • bytes20 recipient : required
  • uint amount : required

Restrictions

  • amount must be greater than zero
  • amount must be less than or equal to internal balance of account linked to sender address
  • recipient must be an existing account
  • recipient must not be the same as username of account linked to sender address

Activate Beneficiary Payout

contract.methods.payBeneficiary(
	'0xc7FF9555C8df69f923C0A72EDdbfd6a079d6D9B3' // Beneficiary address
).send({
	from: accounts[9] // Transaction can be sent from any address
});

payBeneficiary() allows any Ethereum address to trigger a beneficiary payout. View Contract Code

Parameters

  • address beneficiary : required

Restrictions

  • beneficiary funds ready for payout must be greater than zero

World's Fair State

This is where the primary, core state of World's Fair is maintained. You can read data from each of these mappings via the standard compiler-generated getter methods. It's pretty straightforward. For each example, we've included a brief description of what the data represents and, for certain mappings, various considerations you'll want to be familiar with when interpreting the data. By default, if you pass a key to the getter that does not map to anything (such as trying to lookup a nonexistent username by its linked address) your call will return whatever the zero value is for the return type in Solidity.

Asset Data

import WorldsFair from 'worldsfair';
import bigInt from 'big-integer';

const data = await contract.methods.catalogue(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149' // Asset signature
).call();

console.log(data.ipfsHash); // 'QmWn3yBeu8Akm6rwC1zNvHTmvpCjfHchmBQs15YTMawu9S'
console.log(data.http); // 'worldsfair.io'
console.log(web3.utils.hexToUtf8(data.owner)); // 'picklerick'
console.log(data.beneficiary); // '0xc7FF9555C8df69f923C0A72EDdbfd6a079d6D9B3'
console.log(data.timePublished); // '1555690800'
console.log(data.totalShares); // '31416'
console.log(web3.utils.fromWei(data.initialPrice, 'ether')); // '0.01'
console.log(data.pOwner); // '20'
console.log(data.pBeneficiary); // '5'
console.log(bigInt(data.limitConstant).divide(WorldsFair.LIMIT_RESOLUTION).toString()); // '5'
console.log(web3.utils.fromWei(data.valuation)); // '15'

catalogue() returns the contract data for the asset matching the given signature. Note that for limitConstant (due to technical reasons involving integer arithmetic, see the ) the returned value is equal to the actual value of the Appreciation Limiting Constant (ALC) multiplied by LIMIT_RESOLUTION (ten thousand). In the above example, this constant is accessed from the World's Fair module as a static class property and used to divide the raw value so that that actual value of the ALC is printed to the console. View Contract Code

Parameters

  • bytes20 signature : required, signature of asset

Returns

  • string ipfsHash : ipfs multihash of content
  • string http : http link to content
  • bytes20 owner : username of author
  • address beneficiary : beneficiary address
  • uint64 timePublished : unix time of asset creation
  • uint totalShares : number of shares asset is divided into
  • uint initialPrice : ico price of shares
  • uint pOwner : percent of profits author receives as royalites
  • uint pBeneficiary : percent of profits beneficiary receives as royalites
  • uint limitConstant : appreciation limiting constant
  • uint valuation : sum of all profits generated

ICO Calendar

const icoOpen = await contract.methods.calendar(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149' // Signature of asset
).call();

console.log(icoOpen); // '1524266400'

calendar() returns the unix time (according to block timestamp) when an asset's ICO opens. For assets that were set to open their ICO immeditalely upon publication, this method will simply return timePublished. View Contract Code

Paramaters

  • bytes20 signature: required, signature of asset

Returns

  • uint64: unix time when asset's shares become tradable

Account Data

const account = await contract.methods.accounts(
  web3.utils.asciiToHex('picklerick', 20) // Username of account, in bytes20
).call();

console.log(account); // { owner: '0x7fF67C7B94399a4413FE18143580c4F0885D2359', recovery: '0x58e29914027bC34D82C47a8b515d1d9572De04Ed' }

accounts() returns two Etherem addresses: owner (which is the account's linked address) and recovery (the account's recovery address, if they have set one). Note that if you just want to get the account's linked address, you can use getLinkedAddress() instead. View Contract Code

Paramaters

  • bytes20 username: required, account username

Returns

  • bytes20 owner: account's linked address
  • bytes20 recovery: account's recovery address (zero indicates no recovery address)

Account Directory

const username = await contract.methods.directory(
  '0x7fF67C7B94399a4413FE18143580c4F0885D2359' // picklerick's linked address
).call();

console.log(web3.utils.hexToUtf8(username)); // 'picklerick'

directory() returns the username of the account linked to a given address. View Contract Code

Paramaters

  • address addr: required, linked address of account

Returns

  • bytes20 username: username of account

Addresses Encountered

const encountered = await contract.methods.encountered(
  '0x7fF67C7B94399a4413FE18143580c4F0885D2359' // Some address
).call();

console.log(encountered); // true

encountered() returns true if the given address is, or has ever been, linked to an account or set as an account's recovery for this. This mapping exists as a security measure, allowing the contract to enforce a strict single use policy for all addresses (allowing address reuse, while not impossible, would greatly increase the attack surface of World's Fair) This method only returns a boolean telling you if the address has ever been used, but not by which account. If you want to find that out you can query DirRecord events. View Contract Code

Paramaters

  • address addr: required, address to check

Returns

  • bool encountered if that address has ever been used

User Balances

const balance = await contract.methods.balances(
  web3.utils.asciiToHex('picklerick', 20) // Username of account, in bytes20
).call();

console.log(balance); // '1000000000000000000'

balances() returns the current internal balance (in wei) of a given account. View Contract Code

Paramaters

  • bytes20 username: required, username of account

Returns

  • uint balance current internal balance in wei

Beneficiary Balances

const balance = await contract.methods.(
  '0xc7FF9555C8df69f923C0A72EDdbfd6a079d6D9B3' // Beneficiary address
).call();

console.log(balance); // '231678462821374300'

beneficiaries() returns the current balance ready to be paid out to a given beneficiary address. View Contract Code

Paramaters

  • address beneficiary

Returns

  • uint balance

Detecting Events

Events are emitted by the World's Fair contract to make it easy for dapps to detect and synchronize with new data as it's added to the blockchain. All events in Solidity include blockNumber, transactionHash, transactionIndex and several other pieces of information. Additionally, each event type defined in the World's Fair contract includes other data specific to that event. Certain fields are marked as indexed, which means they can be used to query and filter results when searching for events.

Check out the relevant web3 documentation for more information.

New Asset Created

AssetRecord is emitted when an asset is created. View Contract Code

Data

  • bytes20 indexed owner: owner/author of the asset
  • address indexed beneficiary, asset's beneficiary address
  • string ipfsHash: asset's IPFS hash
  • uint64 icoStart: when shares become tradable (unix time)

Canonical Link Modified

HttpModRecord is emitted when the author of an asset edits that canonical link. View Contract Code

Data

  • bytes20 indexed signature: asset signature
  • uint64 time: block timestamp

Account Directory Changed

DirRecord is emitted when a new account is created, a user changes their account's linked address, or when a user activates their recovery address (essentially any time there is a change to the relationship between addresses and accounts in World's Fair). View Contract Code

Data

  • bytes20 indexed username: username of account
  • address oldAddress: previous linked address (zero indicates new account)
  • address indexed newAddress: new linked address
  • bool recovery: true if linked address was changed by activating recovery
  • uint64 time: block timestamp

Asking Price for Asset Shares Lowered

AskModRecord is emitted when a shareholder changes the asking price for their shares, not including the case where they name a new asking price in the course of buying shares. View Contract Code

Data

  • bytes20 indexed signature: asset signature
  • bytes20 indexed seller: username of shareholder
  • uint oldAsk: previous asking price per share
  • uint newAsk: new asking price per share
  • uint64 time: block timestamp

Beneficiary Received Payout

BenPayoutRecord is emitted when a beneficiary payout is activated by a user (when the contract to actually sends ether to the beneficiary address). View Contract Code

Data

  • address indexed beneficiary: beneficiary the received payout
  • bytes20 secondary: username of account that triggered the payout (zero indicates unlinked address)
  • uint amount: amount sent to beneficiary
  • uint64 time: block timestamp

Asset Shares Sold

TxRecord is emitted whene a user buys asset shares. View Contract Code

Data

  • uint shares: number of shares traded
  • uint newAsk: buyer's new asking price
  • uint weiSent: value of transaction
  • uint toSeller: wei credited to seller's balance
  • uint toOwner: wei credited to asset owner's balance
  • uint toBeneficiary: wei credited to beneficiary's balance
  • uint profit: wei generated in profit, valuation increment
  • bytes20 indexed signature: asset signature
  • bytes20 indexed buyer: username of buyer
  • bytes20 indexed seller: username of seller
  • bytes20 owner: username of asset owner
  • address beneficiary: beneficiary address
  • uint64 time: block timestamp

Ether Deposited / Withdrawn / Transferred

BalIORecord is emitted whene a user's internal balance changes not as the result of trading shares (ie deposits, withdrawls, and transfers). View Contract Code

Data

  • bytes20 indexed to: username of recipient
  • bytes20 indexed from: username of sender (zero indicates deposit from unlinked address)
  • uint amount: amount of wei
  • bool foreign: true if not internal transfer
  • uint64 time: block timestamp

ICO Reached Target

IcoTargetReached is emitted when an asset's initial shares are sold at the ICO price (which is to say the asset reaches its Minimum Valuation Target). View Contract Code

Data

  • bytes20 indexed signature: asset signature
  • uint64 time: block timestamp

Read Data Helper Fuctions

Get Current Maximum Buy Price

// Assuming ALC == 5, totalShares == 100, and asset's valuation is 0.5 ETH beyond MVT
const maxBuyPrice = await contract.methods.getMaxBuyPrice(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149' // Signature of asset
).call();

console.log(maxBuyPrice); // '25000000000000000'

getMaxBuyPrice() returns the maximum price that the contract will allow to be paid for shares of asset matching the given signature. After the ICO, maxBuyPrice increases in direct proportion to the asset's valuation (defined as the sum of all that asset's profits) where the proportionality constant is (ALC / totalShares). This function is meant to help dapps implement UI that prevents a user from submitting buy orders with too high a buy price. For assets without an ALC, the maximum is simply the global maximum MAX_SHARE_PRICE. View Contract Code

Parameters

  • bytes20 signature : required

Returns

  • uint maxBuyPrice : current maximum price that can be paid for shares

Get Maximum New Asking Price

// Assuming ALC == 5 and picklerick is not current shareholder
const maxNewAsk = await contract.methods.getMaxNewAsk(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149', // Signature of asset
  web3.utils.asciiToHex('picklerick', 20), // Buyer username, in bytes20
  '100000000000000000' // Buy price / share in wei
).call();

console.log(maxNewAsk); // '500000000000000000'

getMaxNewAsk() returns the maximum new asking price that can be specified when a buyerUsername buys shares of the asset matching the given signature at a certain buyPrice. For assets with an Appreciation Limiting Constant (ALC) maxNewAsk is equal to buyPrice multiplied by the ALC (unless the buyer already owns shares in the asset, in which case maxNewAsk is equal to that buyer's current asking price). This function is meant to help dapps implement UI that prevents a user from submitting buy orders with too high a new asking price. For assets without an ALC, the maximum is simply the global maximum MAX_SHARE_PRICE. View Contract Code

Parameters

  • bytes20 signature : required, signature of asset
  • bytes20 buyerUsername: required, username of buyer
  • uint buyPrice : required, buy price / share

Returns

  • uint maxNewAsk : maximum new asking price when buying shares

Check If Account Exists

const exists = await contract.methods.accountExists(
  web3.utils.asciiToHex('picklerick', 20) // Account username, in bytes20
).call();

console.log(exists); // true

accountExists() returns true if the given username has been linked to an Ethereum address. View Contract Code

Parameters

  • bytes20 username : required, username of account

Returns

  • bool exists : if account exists

Check If Address Is Linked

const isLinked = await contract.methods.addressIsLinked(
  accounts[9] // Ethereum address you want to check
).call();

console.log(isLinked); // false

addressIsLinked() returns true if the given Ethereum address is currently linked to an account. Note that if an address was previously linked to an account but is not linked presently (ie the user changed their linked address) this function will return false. If you want to check if an address has ever been linked, use encountered() instead. View Contract Code

Parameters

  • address addr : required, ethereum address

Returns

  • bool isLinked : if address is currently linked to any account

Get Linked Address Matching Username

const linked = await contract.methods.getLinkedAddress(
  web3.utils.asciiToHex('picklerick', 20) // Username in bytes20
).call();

console.log(linked); // '0xfbE56541f08213C820014AA2a5Da57C2F7f2b192'

getLinkedAddress() returns the address currently linked to the account matching the given username. If the address is not linked to any account, the function returns the zero address. View Contract Code

Parameters

  • bytes20 username : required, username of account

Returns

  • address linked : address linked to username

Get Username Matching Linked Address

const username = await contract.methods.directory(
  '0xfbE56541f08213C820014AA2a5Da57C2F7f2b192'
).call();

console.log(web3.utils.hexToUtf8(username)); // 'picklerick'

directory() returns the username of the account to which the given address is currently linked. If the address is not linked to any account, the function returns zero bytes. View Contract Code

Parameters

  • address addr : required

Returns

  • bytes20 username : username address is linked to

Get Recovery Address of Account

const recovery = await contract.methods.getRecoveryAddress(
  web3.utils.asciiToHex('picklerick', 20) // Username in bytes20
).call();

console.log(linked); // '0x73E074ad47FED8300C5E6674005279E179789E7c'

getRecoveryAddress() returns the recovery address of the account matching the given username. If the account has no recovery address, the function returns the zero address. View Contract Code

Parameters

  • bytes20 username : required

Returns

  • address recovery : address set as recovery

Check If Asset Exists

const exists = await contract.methods.assetExists(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149' // Asset signature
).call();

console.log(exists); // true

assetExists() returns true if the given signature matches an asset that has been created. The signature for any asset can be predetermined (without actually creating the asset) by calling deriveAssetSignature(). View Contract Code

Parameters

  • bytes20 signature : required

Returns

  • bool exists : if asset exists

Derive Asset Signature

import WorldsFair from 'worldsfair';

// Ask the contract to compute the signature
const signature = await contract.methods.deriveAssetSignature(
  'QmWn3yBeu8Akm6rwC1zNvHTmvpCjfHchmBQs15YTMawu9S', // IPFS hash
  web3.utils.asciiToHex('picklerick', 20) // Username of author in bytes20
).call();

// You can also compute the signature locally using JavaScript
const local = WorldsFair.deriveAssetSignature({
  ipfsHash: 'QmWn3yBeu8Akm6rwC1zNvHTmvpCjfHchmBQs15YTMawu9S', // Same hash
  username: 'picklerick' // Username of author in utf8
});

console.log(signature); // '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149';
console.log(signature === local); // true

deriveAssetSignature() returns the signature of the asset uniquely defined by the given IPFS multihash and username. The asset may or may not exist in the World's Fair contract—asset signatures are uniquely and deterministically derived in the pattern (ipfsHash, username) => (signature) so this function simply allows you to find out what the signature must be for the given parameters. The signature can also be computed locally (without calling the contract) via an identically-named static method on the npm module. View Contract Code

Parameters

  • string ipfsHash : required
  • bytes20 username : required

Returns

  • bytes20 signature : signature of asset defined as such

Get Shareholder Model

const data = await contract.methods.getShareholder(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149', // Asset signature
  web3.utils.asciiToHex('morty', 20) // Username of shareholder in bytes20
).call();

console.log('shares', data[0]); // '1500'
console.log('ask', web3.utils.fromWei(data[1], 'ether')); // '0.02'
console.log('weiIn', web3.utils.fromWei(data[2], 'ether')); // '15'
console.log('weiOut', web3.utils.fromWei(data[3], 'ether')); // '0'

getShareholder() returns the contract data for an account's holdings in an asset. View Contract Code

Parameters

  • bytes20 signature : required
  • bytes20 shareholder : required

Returns

  • uint shares : number of shares owned
  • uint ask : asking price
  • uint weiIn : total spent on buying shares
  • uint weiOut : total revenue from selling shares

Check If ICO Complete

const complete = await contract.methods.icoComplete(
  '0xd5fa8790cb17eb20055bf59cae8d48f32dbbb149', // Asset signature
).call();

console.log(complete); // false

icoComplete() returns true if asset has reached its Minimum Valuation Target (MVT) (defined as the number of shares multiplied by the ICO price), which is to say that all shares have been sold by the author, making it possible for a user to buy shares from other shareholders who may be reselling their shares at a premium. If you call this function with a signature matching a nonexistent asset, it will return false. View Contract Code

Parameters

  • bytes20 signature : required

Returns

  • bool true : if asset's ico is complete

Arbitrary Data

The Arbitrary Data API is an effort to provide for maximum extensibility of the World's Fair contract. Mappings for bytes20, string, bool, and uint are offered as tools to help the developers of other contracts and dapps integrate the functionality of their project with elements of World's Fair (ie identity, assets, funds).

Writing Arbitrary Data

// Save an arbitrary string in the World's Fair contract
await contract.methods.setDataString(
  web3.utils.asciiToHex('my_key', 20), // Key must be hex encoded bytes20
  'my_string_value', // Value to save
  true, // Use sender's World's Fair username as identity
  true, // Save the current blocktime in the _time mapping under the same key
  true // Prevent <identity> from ever using <key> to set data again
).send({
  from: accounts[1]
});

You can write data using the setter function for each data type. The above example is for setDataString(), but it works the same way (except for the type of the value paramater, obviously) for setDataBytes, setDataBool(), and setDataUint().

The key used to actually store your value in each mapping is derived from the hash of the provided key and the identity of the transaction sender. This results in a global set of key-value pairs in the form (<key>, <identity>) => <value>.

In addition to the key and value parameters, each setter includes 3 boolean options: Pass the asUser option as true if you prefer to use World's Fair username linked to the sender address, as opposed to the address itself (both are cast to bytes20). timestamp tells the contract to record the current blocktime in the _time mapping when the data is set. Last is the immutably option which allows you to prevent your key from ever being used to set data again (thus making it impossible to overwrite your value).

Note that if you timestamp data, all subsequent transactions to store data using the same key will require that this option be true. View Contract Code

Detecting SetData Events

// Handle all SetData events
contract.events.SetData({}, async (err, data) => {
  // Do whatever you want with data
});

// Only react to events when 'picklerick' sets data
contract.events.SetData({
  filter: { identity: web3.utils.asciiToHex('picklerick', 20) }
}, async (err, data) => {
  // You can filter events by 'key' or 'identity'. See the web3 docs for more info:
  // https://web3js.readthedocs.io/en/1.0/web3-eth-contract.html#id38
});

Whenever data is set, the contract emits a SetData event which includes key, identity, and dataType. The first two data are indexed, so dapps can search blockchain logs for specfic events. View Contract Code

Reading Arbitrary Data

import WorldsFair from 'worldsfair';

// Get key and identity from 'data' in the event callback
const { key, identity } = data.returnValues;

// Ask the World's Fair contract to get the hashed key used to store the value
const lookupKey = await contract.methods.deriveLinkedDataKey(key, identity).call();

// The key can also be computed locally in JavaScript if that's easier
const local = WorldsFair.deriveLinkedDataKey({ key, identity });
console.log(lookupKey === local); // true

// Now you can use that lookup key to fetch the value off the contract
const value = await contract.methods._string(lookupKey).call(); // 'my_string_value'

// You can also lookup metadata (if it exists) in _time and _immutable
// using the same key, if this makes sense for your particular use case
const timestamp = await contract.methods._time(lookupKey).call(); // '1543967411'
const immutable = await contract.methods._immutable(lookupKey).call(); // true

Read data using the getter functions automatically generated by the compiler for each mapping. If you know the identity of the sender who set the data (either by the sender address or their World's Fair username) and the key used to set it, you can call deriveLinkedDataKey(key, identity) to compute the key to use for looking up the data in the mapping. It's also possible to compute this value locally in JavaScript via the identically-named method on the World's Fair npm module. View Contract Code


Solidity API / Contract Code

In this section you can view the most relevant parts of the source code for the World's Fair contract, broken up into sub-components and tagged for improved navigability. If you're looking for the full compilable file, you'll find that here.

Global Constants

uint MAX_SHARES

uint MAX_SHARES = 1000000000; // Maximum 1 billion shares/asset (order of magnitude ~ humans on Earth)

uint MIN_SHARE_PRICE

uint MIN_SHARE_PRICE = 1000000000000; // 1 szabo == 1 trillion wei == 0.000001 ether

uint MAX_SHARE_PRICE

uint MAX_SHARE_PRICE = 100000000000000000000000000; // 100 million ether ~ total supply

uint LIMIT_RESOLUTION

uint LIMIT_RESOLUTION = 10000; // Precision of calculations for Appreciation Limiting Constant

Data Structures

struct Shareholder

struct Shareholder {
    uint shares; // Number of shares owned
    uint ask; // Price at which owner is willing to sell one share
    uint weiIn; // Total wei user spent on buying shares
    uint weiOut; // Total wei user has received from selling shares
}

struct Asset

struct Asset {
    string ipfsHash; // IPFS hash of the data which represents the asset
    string http; // The canonical link to the content
    bytes20 owner; // User that created the asset (author)
    address beneficiary; // Address to which charity contributions will be paid
    uint64 timePublished; // Block timestamp upon publication
    uint totalShares; // Number of shares asset will be divided into
    uint initialPrice; // Initial share price (ico price)
    uint pOwner; // Percent of profits that will be paid to the owner
    uint pBeneficiary; // Percent of profits that will be paid to the beneficiary
    uint limitConstant; // Factor that determines maximum buy / ask price for shares
    uint valuation; // The total profits that have ever been made on this asset
    mapping(bytes20 => Shareholder) shareholders; // Shareholder data
}

struct Account

struct Account {
    address owner; // Linked (managing) address for this user account
    address recovery; // Address that can be used to reassert ownership of this account
}

Event Logs

event AssetRecord

// Asset creation
event AssetRecord(
    bytes20 indexed owner, // Owner/author
    address indexed beneficiary, // Beneficiary address
    string ipfsHash, // IPFS hash of content
    uint64 icoStart // When the ICO opens
);

event HttpModRecord

// Change to asset's canonical http link
event HttpModRecord(
    bytes20 indexed signature, // Asset signature
    uint64 time // Block timestamp
);

event DirRecord

// Account creation, change of linked address
event DirRecord(
    bytes20 indexed username, // Account username
    address oldAddress, // Old linked address
    address indexed newAddress, // New linked address
    bool recovery, // True if recovery event
    uint64 time // Block timestamp
);

event AskModRecord

// Shareholder changed asking price for shares
event AskModRecord(
    bytes20 indexed signature, // Asset signature
    bytes20 indexed seller, // Shareholder username
    uint oldAsk, // Previous asking price
    uint newAsk, // New asking price
    uint64 time // Block timestamp
);

event BenPayoutRecord

// Beneficiary address was sent ether
event BenPayoutRecord(
    address indexed beneficiary, // The address that received the payout
    bytes20 secondary, // The username that triggered the payout
    uint amount, // The amount that was paid
    uint64 time // Block timestamp
);

event TxRecord

// Asset shares changed hands
event TxRecord(
    uint shares, // Number of shares
    uint newAsk, // Buyer's new asking price
    uint weiSent, // Value of transaction
    uint toSeller, // Wei credited to seller's balance
    uint toOwner, // Wei credited to owner's balance
    uint toBeneficiary, // Wei credited to beneficiary's balance
    uint profit, // Wei generated in profit, valuation increment
    bytes20 indexed signature, // Asset signature
    bytes20 indexed buyer, // Username of buyer
    bytes20 indexed seller, // Username of seller
    bytes20 owner, // Username of asset owner
    address beneficiary, // Beneficiary address
    uint64 time // Block timestamp
);

event BalIORecord

// Withdrawls, deposits, transfers
event BalIORecord(
    bytes20 indexed to, // Username of recipient
    bytes20 indexed from, // Username of sender
    uint amount, // Amount of wei
    bool foreign, // True if not internal transfer
    uint64 time // Block timestamp
);

event IcoTargetReached

// Emitted when an asset completes its ICO
event IcoTargetReached(
    bytes20 indexed signature, // Asset signature
    uint64 time // Block timestamp
);

event SetData

// Arbitrary data written to contract
event SetData(
    bytes20 indexed key, // Key (prehash) used to set data
    bytes20 indexed identity, // WorldsFair user or address that set data
    uint8 dataType, // Type of data
    uint64 time // Block timestamp
);

World's Fair State

mapping(bytes20 => Asset) catalogue

// Catalogue of all assets
mapping(bytes20 => Asset) public catalogue;

mapping(bytes20 => uint64) calendar

// When the ico for each asset opens
mapping(bytes20 => uint64) public calendar;

mapping(bytes20 => Account) accounts

// User account data
mapping(bytes20 => Account) public accounts;

mapping(address => bytes20) directory

// Which addresses are currently linked to which username
mapping(address => bytes20) public directory;

mapping(address => bool) encountered

// Addresses which have ever been linked or set as recovery
mapping(address => bool) public encountered;

mapping(bytes20 => uint) balances

// How much ether each account has
mapping(bytes20 => uint) public balances;

mapping(address => uint) beneficiaries

// How much ether is ready to be sent to each beneficiary address
mapping(address => uint) public beneficiaries;

Primary Writable API

function createAccount

// Create a new user account linked to Ethereum address
function createAccount(bytes20 username, address recovery) public {
    
    require(username != bytes20(0x0)); // Username cannot be empty. More than 20 bytes are truncated
    require(!accountExists(username)); // Ensure that account does not already exist
    require(recovery != msg.sender); // Recovery address must not be the same as linked address
    require(!encountered[msg.sender]); // Address must never have been linked before
    require(!encountered[recovery]); // Recovery address must never have been used
    
    accounts[username] = Account({ // Define account model
        owner: msg.sender, // Sender becomes account's linked address
        recovery: recovery // Set recovery address
    });

    // Mark linked address encountered
    setDirectory(msg.sender, username);

    if (recovery != address(0x0)) { // If recovery address was provided
        encountered[recovery] = true; // Record that it has been used
    }

    // Record directory entry
    recordDirectory(username, address(0x0), msg.sender, false);
}

function createAsset

// User can create an asset
function createAsset(
    string ipfsHash,
    string http,
    uint totalShares,
    uint initialPrice,
    address beneficiary,
    uint pOwner,
    uint pBeneficiary,
    uint limitConstant,
    uint64 icoStart
) public {

    require(bytes(ipfsHash).length > 0); // Ensure IPFS hash exists
    require(totalShares > 0); // Asset must have at least one share
    require(totalShares <= MAX_SHARES); // And at most 1 billion shares
    require(initialPrice >= MIN_SHARE_PRICE); // Shares must cost at least one szabo
    require(initialPrice <= MAX_SHARE_PRICE); // And at most 100 million ether
    require(pOwner + pBeneficiary <= uint(100)); // Sum cannot exceed 100%. Logically implies that either one alone cannot exceeed 100%.
    require(limitConstant == 0 || limitConstant >= LIMIT_RESOLUTION); // dMaxPrice/dt cannot be negative, obviously. Zero indicates no upper limit
    require(beneficiary != msg.sender); // You can't name yourself as beneficiary
    
    if (pBeneficiary > 0) { // If you choose to send some ether to beneficiary
        require(beneficiary != address(0x0)); // Beneficiary must exist
    }

    if (beneficiary != address(0x0)) { // If beneficiary exists
        require(pBeneficiary > 0); // You must choose to send some ether
    }

    bytes20 username = directory[msg.sender]; // Lookup linked account name
    require(username != bytes20(0x0)); // Must be linked to an account

    bytes20 signature = deriveAssetSignature(ipfsHash, username); // Hash of account name and ipfsHash
    require(catalogue[signature].owner == bytes20(0x0)); // Ensure asset doesn't already exist
    
    // Save the asset in the catalogue
    catalogue[signature] = Asset({
        ipfsHash: ipfsHash, // IPFS multihash
        http: http, // Canonical link
        owner: username, // Username of author
        timePublished: uint64(now), // Unix time on publication
        totalShares: totalShares, // Total number of shares
        initialPrice: initialPrice, // ICO price
        beneficiary: beneficiary, // Beneficiary address
        pOwner: pOwner, // Percent of profits that author receives
        pBeneficiary: pBeneficiary, // Percent of profits beneficiary receives
        limitConstant: limitConstant, // Appreciation Limiting Constant
        valuation: 0 // No profits generated yet
    });
    
    // Record ICO start time in the calendar
    calendar[signature] = (icoStart > now) ? icoStart : uint64(now);
    
    catalogue[signature].shareholders[username] = Shareholder({
        shares: totalShares, // Author owns all shares initially
        ask: initialPrice, // ICO price
        weiIn: 0, // Zero investment
        weiOut: 0 // Zero revenue
    });
    
    recordAsset(signature); // Emit AssetRecord
}

function buyShares

// User can buy asset shares from another user
function buyShares(
    bytes20 signature,
    bytes20 sellerUsername,
    uint sharesOrdered,
    uint sharePrice,
    uint newAsk,
    bool strict
) public payable {

    bytes20 buyerUsername = directory[msg.sender]; // Get buyer account
    require(buyerUsername != bytes20(0x0)); // Account must exist

    require(now >= calendar[signature]); // You can't buy shares until the ICO is open
    require(sellerUsername != buyerUsername); // You can't buy shares from yourself

    Asset storage asset = catalogue[signature]; // Get the asset
    Shareholder storage seller = asset.shareholders[sellerUsername]; // Get seller model
    Shareholder storage buyer = asset.shareholders[buyerUsername]; // Get buyer model

    // If the seller owns fewer shares than ordered, buy whatever they have left available.
    // Extra ether sent with the transaction will be credited to the buyer's internal balance.
    // Enforcing strict equality might cause annoyance in certain situations where multiple
    // buy orders are submitted simultaneously to the same seller (like during a popular ICO)
    uint numberOfShares = sharesOrdered > seller.shares ? seller.shares : sharesOrdered;
    
    require(numberOfShares > 0); // Order must be for at least one share
    require(!strict || numberOfShares == sharesOrdered); // Optionally require exact shares
    require(asset.owner != buyerUsername); // Author/owner cannot buy shares of their own asset
    require(seller.ask == sharePrice); // Ensure the buyer pays the expected price per share
    require(newAsk >= asset.initialPrice); // New asking price must not be lower than the ICO price

    bool ico = asset.owner == sellerUsername; // Buying shares from author is considered an 'ICO sale'

    // Buying shares in an asset from a seller other than the author/owner is not
    // allowed until the ICO is complete (by definition, an asset attains its
    // minimum valuation when the author has sold all their shares)
    require(ico || asset.valuation >= asset.totalShares * asset.initialPrice);

    if (ico && seller.shares == numberOfShares) { // If buying all author's remaining shares
        emit IcoTargetReached(signature, uint64(now)); // Emit event to indicate ICO completion
    }

    if (asset.limitConstant != 0) { // If asset has an appreciation limiting constant

        // Require that new asking price be no more than the buy price x ALC
        require(newAsk <= (sharePrice * asset.limitConstant) / LIMIT_RESOLUTION);

        // If buyer already owns some shares, require that their new asking
        // price isn't higher than their current ask. This is necessary to
        // prevent the dangerous situation wherein the maximum buy price is
        // lower than the lowest asking price available on the market. Even
        // though the market could get 'unstuck' if those certain shareholders
        // lowered their asking price, we'd rather prevent any one person
        // or group having the ability to effectively freeze trading.
        require(buyer.shares == 0 || newAsk <= buyer.ask);
        
        // Require that buy price be no more than the valuation per share x ALC
        require(ico || sharePrice * asset.totalShares <= (asset.valuation * asset.limitConstant) / LIMIT_RESOLUTION);
    
    } else { // No limiting constant
        require(newAsk < MAX_SHARE_PRICE); // Enforce global maximum
    }

    // Initial checks ok, execute trade logic
    reallocate({
        asset: asset, // Asset model
        signature: signature, // Asset signature
        seller: seller, // Seller model
        buyer: buyer, // Buyer model
        sellerUsername: sellerUsername, // Seller username
        buyerUsername: buyerUsername, // Buyer username
        numberOfShares: numberOfShares, // Number of shares to buy
        newAsk: newAsk // Buyer's new asking price
    });
}

// Execute trade logic
function reallocate(
    Asset storage asset,
    bytes20 signature,
    Shareholder storage seller,
    Shareholder storage buyer,
    bytes20 sellerUsername,
    bytes20 buyerUsername,
    uint numberOfShares,
    uint newAsk
) internal {
    
    // Total price the buyer will pay for the shares
    uint total = numberOfShares * seller.ask;

    // Ensure that buyer has sent enough ether to pay for the shares.
    // This check takes into consideration the buyer's existing balance
    require((balances[buyerUsername] + msg.value) >= total);

    uint profit; // The amount of wei earned beyond that invested
    uint toOwner; // Wei that will be sent to asset owner
    uint toBeneficiary; // Wei that will be sent to specified charity
    uint toSeller; // Wei that the seller of the shares will keep
    
    // Pay royalties only if sale touches the black
    if ((seller.weiOut + total) > seller.weiIn) { 
        
        if (seller.weiOut < seller.weiIn) { // This transaction moves the seller from red to black
            profit = (seller.weiOut + total) - seller.weiIn; // Pay royalties on partial revenue only
        } else { // Seller was already in the black
            profit = total; // Pay royalties on entire sale
        }

        toOwner = (profit * asset.pOwner) / uint(100); // Amount to pay to owner
        toBeneficiary = (profit * asset.pBeneficiary) / uint(100); // Amount to pay to beneficiary
        toSeller = total - (toOwner + toBeneficiary); // Seller gets what's left

    } else { // Sale was entirely in the red
        toSeller = total; // Seller doesn't pay any royalties from revenue
    }
    
    // Sanity check. Ether is like energy, not magic.
    assert(toSeller + toOwner + toBeneficiary == total);
    
    // Update internal ether balances for all parties
    updateBalances(
        signature,
        asset,
        numberOfShares,
        newAsk,
        sellerUsername,
        buyerUsername,
        toSeller,
        toOwner,
        toBeneficiary,
        profit,
        total
    );

    // Reallocate shares
    seller.shares -= numberOfShares;
    buyer.shares += numberOfShares;

    // Update weiIn / weiOut
    seller.weiOut += toSeller;
    buyer.weiIn += total;

    // Set buyer's new asking price
    buyer.ask = newAsk;

    // Profit increments asset valuation
    asset.valuation += profit;
}

// Update internal ether balances for each party in trade
function updateBalances(
    bytes20 signature,
    Asset storage asset,
    uint numberOfShares,
    uint newAsk,
    bytes20 sellerUsername,
    bytes20 buyerUsername,
    uint toSeller,
    uint toOwner,
    uint toBeneficiary,
    uint profit,
    uint total
) internal {

    balances[sellerUsername] += toSeller; // Pay seller
    balances[asset.owner] += toOwner; // Pay author/owner
    beneficiaries[asset.beneficiary] += toBeneficiary; // Pay beneficiary
    
    // Update buyer balance by adding value sent by the transaction and then
    // subtracting the total cost of the order. This means the buyer does not
    // have to send the exact amount to pay for shares. Any extra will simply
    // be credited to their internal account balance.
    balances[buyerUsername] = (balances[buyerUsername] + msg.value) - total;
    
    recordTrade(TradeArgs({ // Emit TxRecord
        signature: signature,
        shares: numberOfShares,
        newAsk: newAsk,
        toSeller: toSeller,
        toOwner: toOwner,
        toBeneficiary: toBeneficiary,
        profit: profit,
        buyer: buyerUsername,
        seller: sellerUsername
    }));
}

function lowerAskingPrice

// Shareholder can lower asking price for shares they already own
function lowerAskingPrice(bytes20 signature, uint newAsk) public {
    bytes20 sellerUsername = directory[msg.sender]; // Get the account name of shareholder
    Asset storage asset = catalogue[signature]; // Get the asset model
    Shareholder storage seller = asset.shareholders[sellerUsername]; // Get the shareholder model
    require(asset.owner != sellerUsername); // Owner can't change the price of ICO shares
    require(seller.shares > 0); // Ensure that shareholder actually holds owns shares
    require(newAsk >= asset.initialPrice); // Require the new asking price is not lower than the ICO price
    require(newAsk < seller.ask); // New price must be lower than current price
    uint oldAsk = seller.ask; // Hold on to the old value so it can be logged
    seller.ask = newAsk; // Everything checks out, make the change
    recordAskMod(signature, sellerUsername, oldAsk, newAsk); // Emit AskModRecord
}

function setCanonicalHttp

// Owner can change the canonical http link of their asset
function setCanonicalHttp(bytes20 signature, string memory http) public {
    Asset storage asset = catalogue[signature]; // Get asset model
    require(asset.owner == directory[msg.sender]); // Sender is asset owner
    asset.http = http; // Overwrite previous link (may be empty string)
    recordHttpMod(signature); // Emit HttpModRecord
}

function changeLinkedAddress

// User can change the linked address for their acconut
function changeLinkedAddress(bytes20 username, address newOwner) public {
    Account storage account = accounts[username]; // Get the account model
    require(account.owner == msg.sender); // Ensure account exists and belongs to sender
    require(!encountered[newOwner]); // Ensure new address was not previously used
    require(newOwner != address(0x0)); // Safety check for zero addresses
    require(newOwner != msg.sender); // Disallow pointless transactions
    directory[account.owner] = bytes20(0x0); // Old address no longer points to account
    account.owner = newOwner; // Set new address as account owner
    setDirectory(newOwner, username); // Mark address encountered
    recordDirectory(username, msg.sender, newOwner, false); // Emit DirRecord
}

function setRecoveryAddress

// User can set the recovery address for their account if they don't have one
function setRecoveryAddress(bytes20 username, address recovery) public {
    Account storage account = accounts[username]; // Lookup the account with the provided key
    require(account.owner == msg.sender); // Ensure account exists and belongs to sender
    require(account.recovery == address(0x0)); // Recovery must not already be set (important!)
    require(recovery != address(0x0)); // Disallow pointless transactions
    require(!encountered[recovery]); // The recovery address must never have been used
    encountered[recovery] = true; // Mark address as encountered
    account.recovery = recovery; // Set the recovery address
}

function recover

// User can reassert ownership of account with recovery address
function recover(bytes20 username) public {
    Account storage account = accounts[username]; // Get the account model
    require(account.recovery == msg.sender); // Ensure recovery address is sender
    directory[account.owner] = bytes20(0x0); // Old address no longer points to anything
    directory[msg.sender] = username; // Recovery address now points to account
    account.recovery = address(0x0); // Reset recovery address in account model
    recordDirectory(username, account.owner, msg.sender, true); // Emit DirRecord
    account.owner = msg.sender; // Recovery address becomes linked address
}

Primary Readable API

function getMaxBuyPrice

// Returns the current maximum price at which shares may be bought
function getMaxBuyPrice(bytes20 signature) public view returns (uint) {
    Asset storage asset = catalogue[signature];
    if (asset.shareholders[asset.owner].shares > 0) { return asset.initialPrice; } // ICO in progress
    return asset.limitConstant == 0 ? MAX_SHARE_PRICE : (asset.valuation * asset.limitConstant) / (asset.totalShares * LIMIT_RESOLUTION);
}

function getMaxNewAsk

// Returns the maximum allowed new asking price when buying shares
function getMaxNewAsk(bytes20 signature, bytes20 buyerUsername, uint buyPrice) public view returns (uint) {
    Asset storage asset = catalogue[signature];
    if (asset.limitConstant == 0) { return MAX_SHARE_PRICE; } // Fall back to global limit
    Shareholder storage buyer = asset.shareholders[buyerUsername];
    return buyer.shares == 0 ? (buyPrice * asset.limitConstant) / LIMIT_RESOLUTION : buyer.ask;
}

function deriveAssetSignature

// Returns calculated asset signature calculated from IPFS multihash and account name
function deriveAssetSignature(string ipfsHash, bytes20 username) public pure returns (bytes20) {
    return bytes20(keccak256(ipfsHash, username));
}

function deriveLinkedDataKey

// Returns key for storing arbitrary linked data for asset and account models
function deriveLinkedDataKey(bytes20 key, bytes20 identity) public pure returns (bytes20) {
    return bytes20(keccak256(key, identity));
}

function accountExists

// Returns true if username has been registered
function accountExists(bytes20 username) public view returns (bool) {
    return accounts[username].owner != address(0x0);
}

function addressIsLinked

// Returns true if given address is currently linked to an account
function addressIsLinked(address addr) public view returns (bool) {
    return directory[addr] != bytes20(0x0);
}

function getLinkedAddress

// Returns linked address given account username
function getLinkedAddress(bytes20 username) public view returns (address) {
    Account storage account = accounts[username];
    return account.owner;
}

function getRecoveryAddress

// Returns recovery address given account username
function getRecoveryAddress(bytes20 username) public view returns (address) {
    Account storage account = accounts[username];
    return account.recovery;
}

function assetExists

// Returns true if given signature matches an existing asset
function assetExists(bytes20 signature) public view returns (bool) {
    return catalogue[signature].owner != bytes20(0x0);
}

function getAsset

// Returns asset data
function getAsset(bytes20 signature) public view returns (string, string, bytes20, address, uint, uint, uint, uint, uint, uint, uint) {
    // Lookup the asset in the catalogue
    Asset storage asset = catalogue[signature];
    return (
        asset.ipfsHash, // 0
        asset.http, // 1
        asset.owner, // 2
        asset.beneficiary, // 3
        asset.timePublished, // 4
        asset.totalShares, // 5
        asset.initialPrice, // 6
        asset.pOwner, // 7
        asset.pBeneficiary, // 8
        asset.limitConstant, // 9
        asset.valuation // 10
    );
}

function getShareholder

// Returns the shareholder model for a given asset and account username
function getShareholder(bytes20 signature, bytes20 shareholder) public view returns (uint, uint, uint, uint) {
    Shareholder storage s = catalogue[signature].shareholders[shareholder];
    return (
        s.shares,
        s.ask,
        s.weiIn,
        s.weiOut
    );
}

function icoComplete

// Returns true if asset's ICO is complete (author has sold all shares)
function icoComplete(bytes20 signature) public view returns (bool) {
    Asset storage asset = catalogue[signature];
    bytes20 owner = asset.owner;
    return asset.owner != bytes20(0x0) && asset.shareholders[owner].shares == 0;
}

Internal Helper Functions

function setDirectory

// Creates an entry in the directory and marks the address as encountered
function setDirectory(address linked, bytes20 username) internal {
    directory[linked] = username;
    encountered[linked] = true;
}

function execMeta

// Handles `timestamp` and `immutably` options
function execMeta(bytes20 key, bool asUser, bool timestamp, bool immutably) internal returns (bytes20) {

    // Can't set based on name if account isn't linked
    require(!asUser || addressIsLinked(msg.sender));
    
    // The key used to store the data is actually the hash of the identity of the transaction
    // sender (sender address or its linked account username) and the provided key, implying
    // a global set of key-value pairs in the form (<key>, <identity>) => <value> for each type
    bytes20 hashKey = deriveLinkedDataKey(key, asUser ? directory[msg.sender] : bytes20(msg.sender));
    
    // Enforce immutability
    require(!_immutable[hashKey]);

    if (immutably) { // Optionally mark immutable
        _immutable[hashKey] = true;
    }

    if (timestamp) { // Optionally timestamp data
        _time[hashKey] = uint64(now); // Update the value
    } else { // Not setting timestamp
        // So key musn't have existing timestamp
        require(_time[hashKey] == 0);
    }
    
    return hashKey;
}

function recordAsset

// Log the creation of an asset
function recordAsset(bytes20 owner, address beneficiary, string memory ipfsHash, uint64 icoStart) internal {
    emit AssetRecord(owner, beneficiary, ipfsHash, icoStart);
}

function recordHttpMod

// Log changes to an asset's http
function recordHttpMod(bytes20 signature) internal {
    emit HttpModRecord(signature, uint64(now));
}

function recordDirectory

// Log changes to which address is linked to which account
function recordDirectory(bytes20 username, address oldAddress, address newAddress, bool recovery) internal {
    emit DirRecord(username, oldAddress, newAddress, recovery, uint64(now));
}

function recordAskMod

// Log when a user lowers the asking price for shares they hold
function recordAskMod(bytes20 signature, bytes20 seller, uint oldAsk, uint newAsk) internal {
    emit AskModRecord(signature, seller, oldAsk, newAsk, uint64(now));
}

function recordTrade

// Create record of shares/funds transferred
function recordTrade(TradeArgs memory r) internal {
    Asset storage asset = catalogue[r.signature];
    emit TxRecord(
        r.shares,
        r.newAsk,
        msg.value,
        r.toSeller,
        r.toOwner,
        r.toBeneficiary,
        r.profit,
        r.signature,
        r.buyer,
        r.seller,
        asset.owner,
        asset.beneficiary,
        uint64(now)
    );
}

function recordIOBalance

// Log change to an address balance
function recordIOBalance(bytes20 to, bytes20 from, uint amount, bool foreign) internal {
    emit BalIORecord(to, from, amount, foreign, uint64(now));
}

function recordBeneficiaryPayout

// Log when funds are sent from the contract to a beneficiary address
function recordBeneficiaryPayout(address beneficiary, bytes20 secondary, uint amount) internal {
    emit BenPayoutRecord(beneficiary, secondary, amount, uint64(now));
}

Withdraw, Deposit, and Transfer Ether

function depositToAccount

// User can deposit ether to someone else's internal balance
function depositToAccount(bytes20 recipient) public payable {
    bytes20 sender = directory[msg.sender]; // Get recipient username
    require(accountExists(recipient)); // Ensure account exists (safety check)
    require(msg.value > 0); // User has sent some ether
    require(recipient != sender); // Use 'depositToSelf' instead
    balances[recipient] += msg.value; // Credit the recipient
    recordIOBalance(recipient, sender, msg.value, true); // Emit BalIORecord
}

function depositToSelf

// User can deposit ether to own internal balance
function depositToSelf() public payable {
    require(addressIsLinked(msg.sender)); // Ensure account exists 
    require(msg.value > 0); // Ensure user has sent some ether
    bytes20 sender = directory[msg.sender]; // Account username
    balances[sender] += msg.value; // Deposit to address balance
    recordIOBalance(sender, sender, msg.value, true); // Emit BalIORecord
}

function withdrawFunds

// User can withdraw ether from the contract
function withdrawFunds(uint amount) public {
    bytes20 sender = directory[msg.sender]; // Get account name
    require(amount > 0); // Disallow pointless transaction
    require(balances[sender] >= amount); // Ensure sufficient balance
    balances[sender] -= amount; // Deduct amount from balance
    recordIOBalance(bytes20(0x0), sender, amount, true); // Emit BalIORecord
    msg.sender.transfer(amount); // Lastly, disburse requested funds
}

function transferFundsInternally

// User can move ether directly from internal balance to another account
function transferFundsInternally(bytes20 recipient, uint amount) public {
    bytes20 sender = directory[msg.sender]; // Get sender username
    require(amount > 0); // Disallow pointless transaction
    require(balances[sender] >= amount); // Sender has sufficent balance
    require(accountExists(recipient)); // Ensure recipient exists (safety check)
    require(recipient != sender); // Can't transfer to same account
    balances[sender] -= amount; // Deduct from sender
    balances[recipient] += amount; // Credit to recipient
    recordIOBalance(recipient, sender, amount, false); // Emit BalIORecord
}

function payBeneficiary

// Anyone can cause accumulated funds to be sent to a beneficiary
function payBeneficiary(address beneficiary) public {
    uint funds = beneficiaries[beneficiary]; // The amount of ether ready to be sent
    require(funds > 0); // Disallow pointless transaction
    beneficiaries[beneficiary] = 0; // Reset beneficiary balance
    recordBeneficiaryPayout(beneficiary, directory[msg.sender], funds); // Emit BenPayoutRecord
    beneficiary.transfer(funds); // Send the funds
}

Arbitrary State

mapping(bytes20 => bool) _immutable

// Mutex locked state changes
mapping(bytes20 => bool) public _immutable;

mapping(bytes20 => uint64) _time

// Timestamped state changes
mapping(bytes20 => uint64) public _time;

mapping(bytes20 => bytes32) _bytes

// Arbitrary bytes
mapping(bytes20 => bytes32) public _bytes;

mapping(bytes20 => string) _string

// Arbitrary strings
mapping(bytes20 => string) public _string;

mapping(bytes20 => bool) _bool

// Arbitrary bools
mapping(bytes20 => bool) public _bool;

mapping(bytes20 => uint) _uint

// Arbitrary uints
mapping(bytes20 => uint) public _uint;

function setDataBytes

function setDataBytes(bytes20 key, bytes32 value, bool asUser, bool timestamp, bool immutably) public {
    _bytes[execMeta(key, asUser, timestamp, immutably)] = value;
    emit SetData(key, asUser ? bytes20(msg.sender) : directory[msg.sender], 0);
}

function setDataString

function setDataString(bytes20 key, string value, bool asUser, bool timestamp, bool immutably) public {
    _string[execMeta(key, asUser, timestamp, immutably)] = value;
    emit SetData(key, asUser ? bytes20(msg.sender) : directory[msg.sender], 1);
}

function setDataBool

function setDataBool(bytes20 key, bool value, bool asUser, bool timestamp, bool immutably) public {
    _bool[execMeta(key, asUser, timestamp, immutably)] = value;
    emit SetData(key, asUser ? bytes20(msg.sender) : directory[msg.sender], 2);
}

function setDataUint

function setDataUint(bytes20 key, uint value, bool asUser, bool timestamp, bool immutably) public {
    _uint[execMeta(key, asUser, timestamp, immutably)] = value;
    emit SetData(key, asUser ? bytes20(msg.sender) : directory[msg.sender], 3);
}
You can’t perform that action at this time.