Last active
January 11, 2018 07:05
-
-
Save AlexanderDzhoganov/c321ddbf2f71f09e3f3e4a4e05f9e990 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Hackmud Token API | |
// | |
// Authors: | |
// nlight (aka facebook.com) | |
// | |
// -- Motivation -- | |
// | |
// The token API allows for the existence of unhackable* user issued in-game currencies running on a network of FULLSEC scripts. | |
// Each implementation (instance) of the API represents a supply of tokens (currency) the ownership of which can be transferred | |
// securely between users. The user who hosts the token script (the issuer) is in full control of the supply and can issue new | |
// tokens at will. The resulting global system is a set of coexisting tokens which can be exchanged by users for GC, services | |
// or other tokens not unlike fiat or crypto currency markets. The value of each token represents the amount of trust the community | |
// has placed in its issuer which will penalize disruptive issuers who act in bad faith i.e. if you print a ton of your money it will | |
// lose value so you likely won't gain anything in the process and assuming the issuer is a large player in the economy he or she | |
// stands to lose a lot by distrupting the token supply. | |
// * Not accounting for bugs in the token implementation or malicious token providers | |
// | |
// Service providers use the token API to provide services to end users in exchange for tokens. Where before a GC exchange would | |
// have occured the user can now pay with tokens instead. A simplified model of the token exchange system looks like | |
// | |
// end user -> service -> token provider | |
// | |
// Where the end user transfers an amount of tokens to a service provider in exchange for a rendered service. | |
// Some service providers will probably wish to exchange any tokens instantly for GC in order to minimize risk which gives the | |
// following view of a more elaborate token system. | |
// | |
// end user -> service -> exchange -> token provider | |
// | |
// Where the end user wants to pay for some service to the service provider with some token. The service provider calls into the | |
// exchange and places a sell order on behalf of the user. The exchange matches the order and executes the sell. In the case of | |
// a token/ token order the exchange just needs to call into the two token APIs to do the transfer. On the other hand when tokens | |
// are exchanged to GC or vice versa a longer process must be followed where the user who has placed the corresponding "buy" order | |
// will escrow (using the in-game escrow service) the GC to the exchange, which is then, in a followup step, responsible for settling | |
// all GC debts with the service providers. This places the responsibility on exchanges who are assumed to act in good faith because | |
// their profits depend on the well being of the token ecosystem. | |
// | |
// An example transaction from the point of view of the user: | |
// | |
// some.service { list: "items_for_sale" } | |
// > .. list of items/ services .. | |
// some.service { buy: "foo_bar" } | |
// > The price for "foo_bar" is 42.5 Mudcoins. Your transaction id is "57f4d3029d26b41d4909f63b". | |
// > [TELL] from mud.token "Your passcode for transaction "57f4d3029d26b41d4909f63b" is "gg3u2a" | |
// some.service { buy: "foo_bar", passcode: "gg3u2a" } | |
// > Thank you for your payment! Your request has been completed. | |
// | |
// -- API Specification -- | |
// | |
// The basic unit of data is a single transaction between a sending account (debit) and a receiving account (credit). | |
// The token database stores a list of all transactions that have occured in the system in chronological order. | |
// A user's current balance is a projection of all his transactions. | |
// Each transaction posesses an identifier "_id" which is unique across all token implementations. | |
// | |
// Transaction { | |
// _id -> string | |
// timestamp -> date | |
// confirmed -> bool | |
// amount -> number | |
// credit -> account | |
// debit -> account (optional) | |
// } | |
// | |
// Each token implementation must adhere to the interface specified below. | |
// | |
// All commands return data in the following format: | |
// { ok: <true/false>, other_data } | |
// On error the API returns a human-readable error message in the "msg" property e.g. | |
// { ok: false, err: "Human-readable error message." } | |
// | |
// The API consists of five commands - issue, send, confirm, get balance and get transaction by id. | |
// | |
// 1. Issue new tokens | |
// | |
// my.token { issue: true, amount: <amount> } | |
// | |
// 2. Create an unconfirmed transaction to send tokens from one account to another. | |
// | |
// my.token { send: true, from: account, to: <account>, amount: <amount> } | |
// | |
// Both "issue" and "send" return an object containing the boolean "ok" indicating the success of the call | |
// and a "transactionId" string property (or a human-readable "msg" property in case of error). | |
// e.g. { ok: true, transactionId: "57f4d3029d26b41d4909f63b" } | |
// | |
// 3. Confirm a transaction. Can only be called directly (not through script) by the sender. | |
// Transactions expire after 3600 seconds (1 hour) and cannot be confirmed after that time. | |
// Account balance is checked at the time of confirmation and the call is rejected if the balance is insufficient. | |
// | |
// my.token { confirm: <transactionId>, passcode: <passcode> } | |
// | |
// We have to consider two separate cases for confirming transactions. | |
// - User-to-user, user-to-service payments. | |
// Implemented by using a side-channel (chats.tell) to transmit a one-time passcode. | |
// - Service-to-user, service-to-service | |
// The directly calling script (context.calling_script) can always confirm its own sends. | |
// | |
// 4. Get the caller's current balance | |
// | |
// my.token { balance: true } | |
// | |
// returns { ok: true, balance: <amount> } | |
// | |
// 5. Get a transaction by id. Returns an error if the caller is not a participant in the transaction. | |
// | |
// my.token { transaction: <transactionId> } | |
// | |
// returns { ok: true, transaction: <Transaction> } | |
// | |
// The script below is a reference implementation of the API | |
// | |
function(context, args) | |
{ | |
if(!args) { | |
args = {} | |
} | |
const TOKEN_ISSUER = "your_user" | |
const ONE_HOUR = 3600 * 1000 // one hour in miliseconds, used later for checking transaction expiry | |
var current_user = context.caller; // get the current user from context.caller | |
if(!current_user) { | |
return { ok: false, msg: "Internal error." } | |
} | |
function nonce(length) { // used to generate a one-time passcode for confirming transactions | |
var nonce = ""; | |
var alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
for(var i = 0; i < length; i++) { | |
nonce += alphabet.charAt(Math.floor(Math.random() * alphabet.length)); | |
} | |
return nonce; | |
} | |
// save a new transaction to the database | |
function save_transaction(transaction) { | |
#db.i(transaction) | |
} | |
// returns a transaction by its transactionId | |
function get_transaction_by_id(id) { | |
var _id = #db.ObjectId() | |
_id.$oid = id // silly trick to convert a string id to a mongo ObjectId() | |
var transaction = #db.f({ | |
_id: _id, | |
$or: [{ | |
credit: current_user | |
}, { | |
debit: current_user | |
}] | |
}).first() | |
return transaction | |
} | |
// issue an amount of tokens. only the TOKEN_ISSUER can call this function | |
function issue_tokens(amount) { | |
amount = parseFloat(amount) | |
if(amount !== amount) { // make sure that amount is not NaN | |
return { ok: false, msg: "Amount is not a number." } | |
} | |
var calling_script_user = null | |
if(context.calling_script) { | |
calling_script_user = context.calling_script.split('.')[0] | |
} | |
if(TOKEN_ISSUER !== current_user && TOKEN_ISSUER !== calling_script_user) { // check if user is authorized to issue tokens | |
return { ok: false, msg: "You are not authorized to issue tokens." } | |
} | |
var transaction = { | |
_id: #db.ObjectId(), // unique identifier of the transaction | |
timestamp: Date.now(), // timestamp for reporting | |
amount: amount, // amount of tokens to issue | |
credit: current_user, // the account to which the issued tokens will be credited | |
confirmed: true // issue transactions are created already confirmed | |
} | |
save_transaction(transaction) // save the new transaction to the database | |
return { ok: true, msg: "Tokens have been issued.", transactionId: transaction._id.$oid } | |
} | |
// creates a new unconfirmed transaction taking tokens from "debit_account" and transferring them to "credit_account" | |
function create_transaction(amount, debit_account, credit_account) { | |
amount = parseFloat(amount) | |
if(amount !== amount) { // check if amount is not NaN | |
return { ok: false, msg: "Amount is not a number." } | |
} | |
if(amount <= 0) { | |
return { ok: false, msg: "Amount must be > 0" } | |
} | |
if(!debit_account) { | |
return { ok: false, msg: "Debit account is null." } | |
} | |
if(!credit_account) { | |
return { ok: false, msg: "Credit account is null." } | |
} | |
if(debit_account === credit_account) { // don't allow sending to the same account | |
return { ok: false, msg: "Cannot credit the same account." } | |
} | |
var passcode = nonce(6) // transaction passcode for confirmation | |
var transaction = { | |
_id: #db.ObjectId(), // unique identifier of the transaction | |
timestamp: Date.now(), // timestamp for reporting | |
amount: amount, // amount of tokens to transfer | |
credit: credit_account, // account which will receive the tokens | |
debit: debit_account, // account from which the tokens will be taken | |
passcode: passcode, // transaction passcode from confirmation (generated above) | |
confirmed: false // transaction is unconfirmed | |
} | |
var calling_script_user = null | |
if(context.calling_script) { | |
calling_script_user = context.calling_script.split('.')[0] | |
} | |
// chats.tell the debiting account his confirmation code | |
if(calling_script_user !== "rep") { | |
#s.chats.tell({ to: current_user, msg: "Your passcode for transaction \"" + transaction._id.$oid + "\" is \"" + passcode + "\"" }) | |
} | |
save_transaction(transaction) // save the new transaction to the database | |
return { ok: true, msg: "Transaction created successfully.", transactionId: transaction._id.$oid } | |
} | |
// confirms a transaction | |
// service-to-user and service-to-service transactions don't have to give a passcode they can confirm directly | |
function confirm_transaction(transactionId, passcode) { | |
var transaction = get_transaction_by_id(transactionId) | |
if(!transaction) { | |
return { ok: false, msg: "Invalid transactionId" } | |
} | |
if(transaction.confirmed) { | |
return { ok: false, msg: "Transaction is already confirmed." } | |
} | |
if((Date.now() - transaction.timestamp) > ONE_HOUR) { | |
return { ok: false, msg: "Transaction has expired" } | |
} | |
var calling_script_user = null | |
if(context.calling_script) { | |
calling_script_user = context.calling_script.split('.')[0] | |
} | |
// handle the two types of confirmation | |
// passcode for user-to-user/ user-to-service payments | |
// context.calling_script for service-to-user/ service-to-service payments | |
if(transaction.passcode !== passcode && calling_script_user !== transaction.debit | |
&& calling_script_user !== "rep") { | |
return { ok: false, msg: "Invalid passcode" } | |
} | |
if(transaction.debit === current_user) { | |
var recent_transaction = #db.f({ | |
debit: current_user, | |
confirmed: true, | |
confirmation_time: { | |
$gte: Date.now() - 5000 | |
} | |
}).first() | |
if(recent_transaction) { | |
return { ok: false, msg: "You confirmed another transaction less than 5 seconds ago. Please wait a few seconds." } | |
} | |
} | |
var balance = get_balance(transaction.debit) | |
if(!balance.ok) { | |
return { ok: false, msg: "Failed to get balance." } | |
} | |
balance = parseFloat(balance.balance) | |
if(balance !== balance) { | |
return { ok: false, msg: "Invalid balance. " } | |
} | |
if(balance < transaction.amount) { | |
return { ok: false, msg: "Insufficient balance in debit account." } | |
} | |
#db.u({ | |
_id: transaction._id | |
}, { | |
$set: { | |
confirmed: true, | |
confirmation_time: Date.now() | |
} | |
}) | |
return { ok: true, msg: "Transaction confirmed.", balance: balance } | |
} | |
// returns the balance of the current user's account calculated as a sum of all his transactions | |
function get_balance(account) { | |
var transactions = #db.f({ // fetch all transactions where the current user is either a sender or a recipient | |
$or: [{ | |
credit: account | |
}, { | |
debit: account | |
}], | |
confirmed: true // we're only interested in confirmed transactions | |
}).array() | |
var balance = 0.0 | |
for(var i = 0; i < transactions.length; i++) { // sum up the list of transactions | |
var transaction = transactions[i] | |
if(!transaction.confirmed) { | |
continue | |
} | |
if(transaction.credit === account) { | |
balance += transaction.amount | |
} else if(transaction.debit === account) { | |
balance -= transaction.amount | |
} | |
} | |
return { ok: true, balance: balance } | |
} | |
if(args.send) { | |
return create_transaction(args.amount, args.from, args.to) | |
} else if(args.confirm) { | |
return confirm_transaction(args.confirm, args.passcode) | |
} else if(args.balance) { | |
return get_balance(current_user) | |
} else if(args.issue) { | |
return issue_tokens(args.amount) | |
} else if(args.transaction) { | |
var transaction = get_transaction_by_id(args.transaction) | |
delete transaction.passcode // always clean-up the confirmation code from transactions returned to the outside world | |
return { ok: true, transaction: transaction } | |
} | |
return { ok: false, msg: "No command specified." } | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment