Permalink
Browse files

Migrate to new input/output and signing structure as per FAT-0 commit…

… e596ceb088d9111da69c08d83dab2564a7409b66. Rename RPC => CLI. Temporarily disable integration tests
  • Loading branch information...
drkatz committed Dec 5, 2018
1 parent 5a362a9 commit 22f2d8db817a4cefd8c20a3da62a0695f4c10981
Showing with 96 additions and 134 deletions.
  1. +34 −57 0/Transaction.js
  2. +8 −8 README.md
  3. +20 −21 rpc/RPC.js → cli/CLI.js
  4. +3 −3 dist/fatjs.js
  5. +1 −1 index.js
  6. +9 −9 test/0/0.integration.spec.js
  7. +20 −34 test/0/0.unit.spec.js
  8. +1 −1 test/test.js
@@ -11,26 +11,24 @@ const COINBASE_ADDRESS_PUBLIC = 'FA1zT4aFpEvcnPqPCigB3fvGu4Q4mTXY22iiuV69DqE1pNh
const COINBASE_ADDRESS_PRIVATE = 'Fs1KWJrpLdfucvmYwN2nWrwepLn8ercpMbzXshd1g8zyhKXLVLWj';

class TransactionBuilder {
constructor(builder) {
if (builder instanceof TransactionBuilder) {
this._keys = builder._keys;
this._inputs = builder._inputs;
this._outputs = builder._outputs;
this._blockheight = builder._blockheight;
this._salt = builder._salt;
} else {
this._keys = [];
this._inputs = [];
this._outputs = [];
}
constructor(tokenId, rootChainId) {
if (typeof tokenId !== 'string' || tokenId.length < 1) throw new Error("Token ID must be a string with a minimum length of 1");
this._tokenId = tokenId;

if (!fctIdentityUtil.isValidIdentityChainId(rootChainId)) throw new Error("You must include a valid Root Chain ID to create a coinbase transaction");
this._rootChainId = rootChainId;

this._keys = [];
this._inputs = {};
this._outputs = {};
}

input(fs, amount) {
if (!fctAddressUtil.isValidPrivateAddress(fs)) throw new Error("Input address must be a valid private Factoid address");
if (isNaN(amount) || !Number.isInteger(amount) || amount < 1) throw new Error("Input amount must be a positive nonzero integer");

this._keys.push(nacl.keyPair.fromSeed(fctAddressUtil.addressToKey(fs)));
this._inputs.push({address: fctAddressUtil.getPublicAddress(fs), amount: amount});
this._inputs[fctAddressUtil.getPublicAddress(fs)] = amount;
return this;
}

@@ -43,40 +41,25 @@ class TransactionBuilder {
if (!fctAddressUtil.isValidFctPublicAddress(fa)) throw new Error("Input address must be a valid public Factoid address");
if (isNaN(amount) || !Number.isInteger(amount) || amount < 1) throw new Error("Input amount must be a positive nonzero integer");

this._outputs.push({address: fa, amount: amount});
this._outputs[fa] = amount;
return this;
}

setIssuerInformation(rootChainId, tokenId, sk1) {
if (!fctIdentityUtil.isValidIdentityChainId(rootChainId)) throw new Error("You must include a valid Root Chain ID to create a coinbase transaction");
setIssuerSK1(sk1) {
if (!fctIdentityUtil.isValidSk1(sk1)) throw new Error("You must include a valid SK1 Key to sign a coinbase transaction");
this._rootChainId = rootChainId;
this._sk1 = sk1;
this._tokenId = tokenId;
return this;
}

blockheight(blockheight) {
if (isNaN(blockheight) || !Number.isInteger(blockheight)) throw new Error("Blockheight must be a positive nonzero integer");
this._blockheight = blockheight;
return this;
}

salt(salt) {
if (!typeof salt === 'string' || !salt instanceof String) throw new Error("Salt must be a string");
this._salt = salt;
return this;
}

build() {
if (!Number.isInteger(this._blockheight) || this._blockheight < 0) throw new Error("Blockheight is required, and must be an unsigned integer");
if (Object.keys(this._inputs).length === 0 || Object.keys(this._outputs).length === 0) throw new Error("Must have at least one input and one output");

if (this._inputs.length === 0 || this._outputs.length === 0) throw new Error("Must have at least one input and one output");
const inputSum = Object.values(this._inputs).reduce((amount, sum) => amount + sum, 0);
const outputSum = Object.values(this._outputs).reduce((amount, sum) => amount + sum, 0);
if (inputSum !== outputSum) throw new Error("Input and output amount sums must match (" + inputSum + " != " + outputSum + ")");

const inputSum = this._inputs.reduce((input, sum) => input.amount + sum, 0);
const outputSum = this._outputs.reduce((output, sum) => output.amount + sum, 0);
this._salt = crypto.randomBytes(32).toString('hex');

if (inputSum !== outputSum) throw new Error("Input and output amount sums must match (" + inputSum + " != " + outputSum + ")");
return new Transaction(this);
}
}
@@ -89,44 +72,40 @@ class Transaction {
if (builder instanceof TransactionBuilder) {
this.inputs = builder._inputs;
this.outputs = builder._outputs;
this.blockheight = builder._blockheight;
this.salt = builder._salt || crypto.randomBytes(32).toString('hex');
this.salt = builder._salt;

this.content = JSON.stringify(this);

this.extIds = [];

if (builder._keys.length > 0) {
this.rcds = builder._keys.map(key => Buffer.concat([RCD_TYPE_1, Buffer.from(key.publicKey)]));
this.signatures = builder._keys.map(key => Buffer.from(nacl.detached(Buffer.from(this.content), key.secretKey)));
let sigIndexCounter = 1;
this.signatures = builder._keys.map(key => {
let signature = Buffer.from(nacl.detached(Buffer.from(sigIndexCounter.toString() + util.getTransactionChainId(builder._tokenId, builder._rootChainId) + this.content), key.secretKey));
sigIndexCounter += 2;
return signature;
});
for (let i = 0; i < rcds.length; i++) {
this.extIds.push(rcds[i]);
this.extIds.push(signatures[i]);
}
}

//handle coinbase tx
if (this.inputs.find(input => input.address === COINBASE_ADDRESS_PUBLIC)) {
if (Object.keys(this.inputs).find(address => address === COINBASE_ADDRESS_PUBLIC)) {
if (!builder._sk1) throw new Error("You must include a valid SK1 Key to sign a coinbase transaction");
this.extIds.push(fctIdentityCrypto.sign(builder._sk1, util.getTransactionChainId(builder._tokenId, builder._rootChainId) + this.content));
}

validateRcds(this.inputs, this.rcds);
validateSignatures(Buffer.from(this.content), this.rcds, this.signatures);
// validateSignatures(Buffer.from(this.content), this.rcds, this.signatures);
} else if (typeof builder === 'object') {
this.txId = builder.txId;
this.inputs = builder.inputs;
this.outputs = builder.outputs;
this.blockheight = builder.blockheight;
this.timestamp = builder.timestamp;
this.salt = builder.salt;

this.content = JSON.stringify({
inputs: this.inputs,
outputs: this.outputs,
milliTimestamp: this.timestamp,
salt: this.salt
});
this.timestamp = builder.timestamp;
}

Object.freeze(this);
@@ -144,10 +123,6 @@ class Transaction {
return this.timestamp;
}

getBlockheight() {
return this.blockheight;
}

getSalt() {
return this.salt;
}
@@ -165,13 +140,13 @@ class Transaction {
}

isCoinbase() {
return this.inputs.find(input => input.address === COINBASE_ADDRESS_PUBLIC) !== undefined;
return Object.keys(this.inputs).find(address => address === COINBASE_ADDRESS_PUBLIC) !== undefined;
}

isValid() {
try {
validateRcds(this.inputs, this.rcds);
validateSignatures(Buffer.from(this.content), this.rcds, this.signatures);
// validateSignatures(Buffer.from(this.content), this.rcds, this.signatures);
return true;
} catch (e) {
return false;
@@ -184,11 +159,13 @@ class Transaction {
}

function validateRcds(inputs, rcds) {
if (rcds.length !== inputs.length) {
if (rcds.length !== Object.keys(inputs).length) {
throw new Error(`The number of RCDs (${rcds.length}) does not equal the number of inputs (${inputs.length}).`);
}
for (let i = 0; i < rcds.length; ++i) {
validateRcdHash(inputs[i], rcds[i]);
const input = {address: Object.keys(inputs)[i], amount: Object.values(inputs)[i]};

validateRcdHash(input, rcds[i]);
}
}

@@ -6,7 +6,7 @@

[![Coverage Status](https://coveralls.io/repos/github/DBGrow/fat-js/badge.svg?branch=master&t=X5s8cd)](https://coveralls.io/github/DBGrow/fat-js?branch=master)

[Factom Asset Token](https://github.com/DBGrow/FAT) Client and RPC implementation in JS :blue_heart:
[Factom Asset Token](https://github.com/DBGrow/FAT) Client and CLI implementation in JS :blue_heart:

Currently supports **FAT-0** token standard.

@@ -46,24 +46,24 @@ A fresh `bundle.js` will be built in the `browser` directory.



## Instantiate FAT RPC
## Instantiate FAT CLI

```javascript
const RPCBuilder = require('fat-js').RPCBuilder
let rpc = new RPCBuilder()
const CLIBuilder = require('fat-js').CLIBuilder
let rpc = new CLIBuilder()
.host('fatnode.mysite.com')
.port(1234)
.version('v0')
.auth('my-user', 'my-pass')
.build();
//get the RPC client for a token: <tokenId> <issuer Root Chain ID>
//get the CLI client for a token: <tokenId> <issuer Root Chain ID>
let tokenRPC = rpc.getTokenRPC('mytoken','888888d027c59579fc47a6fc6c4a5c0409c7c39bc38a86cb5fc0069978493762')
```



## FAT Token RPC Calls
## FAT Token CLI Calls

### Get Issuance

@@ -171,7 +171,7 @@ console.log(JSON.stringify(stats, undefined, 2));
All FAT-0 Transaction Builder Options

```javascript
const RPCBuilder = require('fat-js').FAT0.TransactionBuilder
const CLIBuilder = require('fat-js').FAT0.TransactionBuilder
let tx = new TransactionBuilder()
.input("Fs1q7FHcW4Ti9tngdGAbA3CxMjhyXtNyB1BSdc8uR46jVUVCWtbJ", 75)
.input("Fs1q7FHcW4Ti9tngdGAbA3CxMjhyXtNyB1BSdc8uR46jVUVCWtbJ", 75)
@@ -188,7 +188,7 @@ let tx = new TransactionBuilder()
All FAT-0 Issuance Builder Options

```javascript
const RPCBuilder = require('fat-js').FAT0.IssuanceBuilder
const CLIBuilder = require('fat-js').FAT0.IssuanceBuilder
let issuance = new IssuanceBuilder("888888d027c59579fc47a6fc6c4a5c0409c7c39bc38a86cb5fc0069978493762", "mytoken", "sk11pz4AG9XgB1eNVkbppYAWsgyg7sftDXqBASsagKJqvVRKYodCU")
.supply(1000000)
.name('Test Token')
@@ -2,7 +2,7 @@ const axios = require('axios');
const fctIdentityUtil = require('factom-identity-lib/src/validation');
const fctAddressUtil = require('factom/src/addresses');

class RPCBuilder {
class CLIBuilder {
constructor(builder) {

}
@@ -26,27 +26,27 @@ class RPCBuilder {
}

build() {
return new RPC(this);
return new CLI(this);
}
}

class RPC {
class CLI {
constructor(builder) {
if (!builder instanceof RPCBuilder) throw new Error("Must include an rpc builder");
if (!builder instanceof CLIBuilder) throw new Error("Must include an cli builder");
this._host = builder._host || 'localhost';
this._port = builder._port || 8078;
this._username = builder._username;
this._password = builder._password;
}

getTokenRPC(tokenId, rootChainId) {
return new BaseTokenRPC(this, tokenId, rootChainId);
return new BaseTokenCLI(this, tokenId, rootChainId);
}

getTypedTokenRPC(type, tokenId, rootChainId) {
switch (type) {
case 'FAT-0': {
return new FAT0TokenRPC(this, tokenId, rootChainId);
return new FAT0TokenCLI(this, tokenId, rootChainId);
}
default:
throw new Error("Unsupported FAT token type " + type);
@@ -62,31 +62,31 @@ class RPC {
}
}

class BaseTokenRPC {
class BaseTokenCLI {
constructor(rpc, tokenId, rootChainId) {
if (!rpc instanceof RPC) throw new Error("Must include an RPc object of type RPC");
if (!rpc instanceof CLI) throw new Error("Must include an RPc object of type CLI");
this._rpc = rpc;

if (tokenId === undefined || typeof tokenId !== 'string') throw new Error('Token is a required string');
this._tokenId = tokenId;

if (!fctIdentityUtil.isValidIdentityChainId(rootChainId)) throw new Error("You must include a valid issuer identity Root Chain Id construct BaseTokenRPC");
if (!fctIdentityUtil.isValidIdentityChainId(rootChainId)) throw new Error("You must include a valid issuer identity Root Chain Id construct BaseTokenCLI");
this._rootChainId = rootChainId;
}

getIssuance() {
return call(this._rpc, 'get-issuance', generateTokenRPCParams(this));
return call(this._rpc, 'get-issuance', generateTokenCLIParams(this));
}

getTransaction(txId) {
if (txId.length !== 64) throw new Error("You must include a valid 32 Byte tx ID (entryhash)");
return call(this._rpc, 'get-transaction', generateTokenRPCParams(this, {'tx-id': txId}));
return call(this._rpc, 'get-transaction', generateTokenCLIParams(this, {'tx-id': txId}));
}

getTransactions(txId, fa, start, limit) {
if (txId && txId.length !== 32) throw new Error("You must include a valid 32 Byte tx ID (entryhash)");
if (fa && !fctAddressUtil.isValidFctPublicAddress(fa)) throw new Error("You must include a valid public Factoid address");
return call(this._rpc, 'get-transactions', generateTokenRPCParams(this, {
return call(this._rpc, 'get-transactions', generateTokenCLIParams(this, {
'tx-id': txId,
'fa-address': fa,
start: start,
@@ -96,24 +96,24 @@ class BaseTokenRPC {

getBalance(fa) {
if (!fctAddressUtil.isValidFctPublicAddress(fa)) throw new Error("You must include a valid public Factoid address");
return call(this._rpc, 'get-balance', generateTokenRPCParams(this, {'fa-address': fa}));
return call(this._rpc, 'get-balance', generateTokenCLIParams(this, {'fa-address': fa}));
}

getStats() {
return call(this._rpc, 'get-stats', generateTokenRPCParams(this));
return call(this._rpc, 'get-stats', generateTokenCLIParams(this));
}

//non-fungible
getToken(tokenId) {
return call(this._rpc, 'get-token', generateTokenRPCParams(this, {'nf-token-id': tokenId}));
return call(this._rpc, 'get-token', generateTokenCLIParams(this, {'nf-token-id': tokenId}));
}
}

//Token Specific Token RPCs (Optional, wraps response in class from corresponding token type)
let FAT0Transaction = require('../0/Transaction').Transaction;
let FAT0Issuance = require('../0/Issuance').Issuance;

class FAT0TokenRPC extends BaseTokenRPC {
class FAT0TokenCLI extends BaseTokenCLI {
constructor(rpc, rootChainId, tokenId) {
super(rpc, rootChainId, tokenId);
}
@@ -135,16 +135,15 @@ class FAT0TokenRPC extends BaseTokenRPC {
}



function generateTokenRPCParams(tokenRPC, params) {
function generateTokenCLIParams(tokenRPC, params) {
return Object.assign({
'token-id': tokenRPC._tokenId,
'issuer-id': tokenRPC._rootChainId
}, params);
}

async function call(rpc, method, params) {
if (!rpc instanceof RPC) throw new Error("Must include a valid RPC instance to call endpoint");
if (!rpc instanceof CLI) throw new Error("Must include a valid CLI instance to call endpoint");

//TODO: Basic HTTP Auth

@@ -163,6 +162,6 @@ async function call(rpc, method, params) {
}

module.exports = {
RPCBuilder,
BaseTokenRPC
CLIBuilder,
BaseTokenCLI
};

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -1,5 +1,5 @@
module.exports = Object.assign({},
require('./rpc/RPC'),
require('./cli/CLI'),
{
FAT0: {
Transaction: require('./0/Transaction'),
Oops, something went wrong.

0 comments on commit 22f2d8d

Please sign in to comment.