Skip to content

Instantly share code, notes, and snippets.

@SiestaMadokaist
Last active September 24, 2018 04:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SiestaMadokaist/d835318bbe9874e076aade2172ea6b3f to your computer and use it in GitHub Desktop.
Save SiestaMadokaist/d835318bbe9874e076aade2172ea6b3f to your computer and use it in GitHub Desktop.
Nonce-based Transactional Database

Table UserTransactions

account_id {String} nonce {Integer} updates {Integer} balance {Integer} previous {Integer} Description {String}
0000001 0 0 0 0 Initialization
0000001 1 10 10 0 Deposit from ....

Constraint:

  • UNIQUE(AccountId, Nonce) prevent double spending
  • CHECK(balance >= 0) sanity check
  • CHECK(previous + updates = balance) sanity check extra, because we're hyperparanoid

Actions

class UserTransaction extends SomeMySQLLibrary {
  fetchCurrentBalance(){
      return this.query(`SELECT * FROM UserTransactions WHERE AccountId = ${this.accountId()} ORDER BY Nonce DESC`)
  }
          
  /**
  * @params oldTx {UserTransaction}
  * @params update {Integer}
  */
  createNewTx(update){
     const balance = this.balance() + update;
     const nonce = this.nonce() + 1;
     const props = Object.assign({}, this.props, {Balance: balance, Update: update, Nonce: nonce });
     return new UserTransaction(props);
  }
                                                
  /**
  * @params targetUserTx {UserTransaction}
  * @params amount {Integer}
  */                                                
  transferWithJS(targetUserBalance, amount){
      const myNewBalance = this.createNewTx(myCurrentBalance, amount * -1);
      const targetNewBalance = targetUserTx.createNewTx(targetCurrentBalance, amount);
      return DB.runTransaction(() => {
         myNewBalance.save();
         targetNewBalance.save();
      }).catch((error, RecordNotUniqueError) => {
         // todo: handle if the record failed to be saved because UserId+Nonce index unique constraint doesn't satisfied.
         // or just throw error, and let the user handle that.
         // or you can try to retry it yourself.
      });
  }
  
  transferWithMySQL(targetAccountId, amount, description){
     const query = `
      START TRANSACTION;

      INSERT INTO user_transactions (account_id, nonce, updates, balance, description, previous)
      SELECT t1.account_id, t1.nonce + 1, ${amount * -1}, t1.balance + ${amount * -1}, ${description}, t1.balance
      FROM user_transactions t1
      WHERE t1.account_id = ${this.accountId()} AND nonce = (
        SELECT max(nonce) from user_transactions WHERE (account_id = ${this.accountId()})
      );

      INSERT INTO user_transactions (account_id, nonce, updates, balance, description, previous)
      SELECT t1.account_id, t1.nonce + 1, ${amount}, t1.balance + ${amount}, ${description}, t1.balance
      FROM user_transactions t1
      WHERE t1.account_id = ${targetAccountId} AND nonce = (
        SELECT max(nonce) from user_transactions WHERE (account_id = ${targetAccountId})
      );

      COMMIT;
     `;
     // TODO: sanitize the query first
     return this.query(query);
  }
  // .... more code ....
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment