Skip to content

Instantly share code, notes, and snippets.

@AlexanderDzhoganov
Last active January 11, 2018 07:05
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save AlexanderDzhoganov/c321ddbf2f71f09e3f3e4a4e05f9e990 to your computer and use it in GitHub Desktop.
Save AlexanderDzhoganov/c321ddbf2f71f09e3f3e4a4e05f9e990 to your computer and use it in GitHub Desktop.
// 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