Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions modules/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"debug": "^3.1.0",
"ecurve": "^1.0.6",
"eol": "^0.5.0",
"eosjs-ecc": "4.0.4",
"lodash": "^4.17.11",
"moment": "^2.20.1",
"prova-lib": "^0.2.10",
Expand Down
4 changes: 4 additions & 0 deletions modules/core/src/v2/coinFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { Bsv } from './coins/bsv';
import { Btc } from './coins/btc';
import { Btg } from './coins/btg';
import { Dash } from './coins/dash';
import { Eos } from './coins/eos';
import { Eth } from './coins/eth';
import { Ltc } from './coins/ltc';
import { Ofc } from './coins/ofc';
Expand All @@ -20,6 +21,7 @@ import { Tbsv } from './coins/tbsv';
import { Tbtc } from './coins/tbtc';
import { Tbtg } from './coins/tbtg';
import { Tdash } from './coins/tdash';
import { Teos } from './coins/teos';
import { Teth } from './coins/teth';
import { Tltc } from './coins/tltc';
import { Trmg } from './coins/trmg';
Expand Down Expand Up @@ -104,6 +106,8 @@ GlobalCoinFactory.registerCoinConstructor('btg', Btg.createInstance);
GlobalCoinFactory.registerCoinConstructor('tbtg', Tbtg.createInstance);
GlobalCoinFactory.registerCoinConstructor('ltc', Ltc.createInstance);
GlobalCoinFactory.registerCoinConstructor('tltc', Tltc.createInstance);
GlobalCoinFactory.registerCoinConstructor('eos', Eos.createInstance);
GlobalCoinFactory.registerCoinConstructor('teos', Teos.createInstance);
GlobalCoinFactory.registerCoinConstructor('eth', Eth.createInstance);
GlobalCoinFactory.registerCoinConstructor('teth', Teth.createInstance);
GlobalCoinFactory.registerCoinConstructor('rmg', Rmg.createInstance);
Expand Down
220 changes: 220 additions & 0 deletions modules/core/src/v2/coins/eos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* @prettier
*/
import { BaseCoin } from '../baseCoin';
import { BigNumber } from 'bignumber.js';
import * as crypto from 'crypto';
import * as utxoLib from 'bitgo-utxo-lib';
import * as url from 'url';
import * as querystring from 'querystring';
import * as _ from 'lodash';
import { InvalidAddressError, UnexpectedAddressError } from '../../errors';

const EOS_ADDRESS_LENGTH = 12;

interface KeyPair {
pub: string;
prv: string;
}

interface AddressDetails {
address: string;
memoId: string;
}

export class Eos extends BaseCoin {
static createInstance(bitgo: any): BaseCoin {
return new Eos(bitgo);
}

getChain(): string {
return 'eos';
}

getFamily(): string {
return 'eos';
}

getFullName(): string {
return 'EOS';
}

getBaseFactor(): number {
return 1e4;
}

/**
* Flag for sending value of 0
* @returns {boolean} True if okay to send 0 value, false otherwise
*/
valuelessTransferAllowed(): boolean {
return true;
}

/**
* Generate secp256k1 key pair
*
* @param seed - Seed from which the new keypair should be generated, otherwise a random seed is used
*/
generateKeyPair(seed?: Buffer): KeyPair {
if (!seed) {
// An extended private key has both a normal 256 bit private key and a 256
// bit chain code, both of which must be random. 512 bits is therefore the
// maximum entropy and gives us maximum security against cracking.
seed = crypto.randomBytes(512 / 8);
}
const extendedKey = utxoLib.HDNode.fromSeedBuffer(seed);
const xpub = extendedKey.neutered().toBase58();
return {
pub: xpub,
prv: extendedKey.toBase58(),
};
}

/**
* Return boolean indicating whether input is valid public key for the coin.
*
* @param pub - the pub to be checked
*/
isValidPub(pub: string): boolean {
try {
utxoLib.HDNode.fromBase58(pub);
return true;
} catch (e) {
return false;
}
}

/**
* Return boolean indicating whether input is valid seed for the coin
*
* @param prv - the prv to be checked
*/
isValidPrv(prv: string): boolean {
try {
utxoLib.HDNode.fromBase58(prv);
return true;
} catch (e) {
return false;
}
}

/**
* Evaluates whether a memo is valid
*
* @param memo - the memo to be checked
*/
isValidMemo(memo: string): boolean {
return memo && memo.length <= 256;
}

/**
* Return boolean indicating whether a memo id is valid
*
* @param memoId - the memo id to be checked
*/
isValidMemoId(memoId: string): boolean {
if (!this.isValidMemo(memoId)) {
return false;
}

let memoIdNumber;
try {
memoIdNumber = new BigNumber(memoId);
} catch (e) {
return false;
}

return memoIdNumber.gte(0);
}

/**
* Process address into address and memo id
* @param address - the address
*/
getAddressDetails(address: string): AddressDetails {
const destinationDetails = url.parse(address);
const queryDetails = querystring.parse(destinationDetails.query);
const destinationAddress = destinationDetails.pathname;

// EOS addresses have to be "human readable", which means start with a letter, up to 12 characters and only a-z1-5., i.e.mtoda1.bitgo
// source: https://developers.eos.io/eosio-cpp/docs/naming-conventions
if (!/^[a-z][a-z1-5.]*$/.test(destinationAddress) || destinationAddress.length > EOS_ADDRESS_LENGTH) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the spec is less than 12, then this condition is wrong and should be >=. What should the behavior be if the address is exactly 12 characters?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's up to 12 characters. Fixed the comment

throw new InvalidAddressError(`invalid address: ${address}`);
}

// address doesn't have a memo id
if (destinationDetails.pathname === address) {
return {
address: address,
memoId: null,
};
}

if (!queryDetails.memoId) {
// if there are more properties, the query details need to contain the memoId property
throw new InvalidAddressError(`invalid property in address: ${address}`);
}

if (Array.isArray(queryDetails.memoId) && queryDetails.memoId.length !== 1) {
// valid addresses can only contain one memo id
throw new InvalidAddressError(`invalid address '${address}', must contain exactly one memoId`);
}

const [memoId] = _.castArray(queryDetails.memoId);
if (!this.isValidMemoId(memoId)) {
throw new InvalidAddressError(`invalid address: '${address}', memoId is not valid`);
}

return {
address: destinationAddress,
memoId,
};
}

/**
* Validate and return address with appended memo id
*
* @param address
* @param memoId
*/
normalizeAddress({ address, memoId }: AddressDetails): string {
if (this.isValidMemoId(memoId)) {
return `${address}?memoId=${memoId}`;
}
return address;
}

/**
* Return boolean indicating whether input is valid public key for the coin
*
* @param address - the address to be checked
*/
isValidAddress(address: string): boolean {
try {
const addressDetails = this.getAddressDetails(address);
return address === this.normalizeAddress(addressDetails);
} catch (e) {
return false;
}
}

/**
* Check if address is a valid EOS address, then verify it matches the root address.
*
* @param address - the address to verify
* @param rootAddress - the wallet's root address
*/
verifyAddress({ address, rootAddress }: { address: string; rootAddress: string }): void {
if (!this.isValidAddress(address)) {
throw new InvalidAddressError(`invalid address: ${address}`);
}

const addressDetails = this.getAddressDetails(address);
const rootAddressDetails = this.getAddressDetails(rootAddress);

if (addressDetails.address !== rootAddressDetails.address) {
throw new Error(`address validation failure: ${addressDetails.address} vs ${rootAddressDetails.address}`);
}
}
}
17 changes: 17 additions & 0 deletions modules/core/src/v2/coins/teos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { BaseCoin } from '../baseCoin';
import { Eos } from './eos';

export class Teos extends Eos {

static createInstance(bitgo: any): BaseCoin {
return new Teos(bitgo);
}

getChain() {
return 'teos';
}

getFullName() {
return 'Testnet EOS';
}
}
98 changes: 98 additions & 0 deletions modules/core/test/v2/unit/coins/eos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import * as should from 'should';
import * as crypto from 'crypto';

const TestV2BitGo = require('../../../lib/test_bitgo');

describe('EOS:', function() {
let bitgo;
let basecoin;

before(function() {
bitgo = new TestV2BitGo({ env: 'test' });
bitgo.initializeTestVars();
basecoin = bitgo.coin('teos');
});

it('should get address details', function() {
let addressDetails = basecoin.getAddressDetails('i1skda3kso43');
addressDetails.address.should.equal('i1skda3kso43');
should.not.exist(addressDetails.memoId);

addressDetails = basecoin.getAddressDetails('ks13k3hdui24?memoId=1');
addressDetails.address.should.equal('ks13k3hdui24');
addressDetails.memoId.should.equal('1');

(() => { basecoin.getAddressDetails('ks13k3hdui24?memoId=x'); }).should.throw();
(() => { basecoin.getAddressDetails('ks13k3hdui24?memoId=1&memoId=2'); }).should.throw();
});

it('should validate address', function() {
basecoin.isValidAddress('i1skda3kso43').should.equal(true);
basecoin.isValidAddress('ks13kdh245ls').should.equal(true);
basecoin.isValidAddress('ks13k3hdui24?memoId=1').should.equal(true);
basecoin.isValidAddress('ks13k3hdui24?memoId=x').should.equal(false);
});

it('verifyAddress should work', function() {
basecoin.verifyAddress({
address: 'i1skda3kso43',
rootAddress: 'i1skda3kso43',
});
basecoin.verifyAddress({
address: 'ks13kdh245ls?memoId=1',
rootAddress: 'ks13kdh245ls',
});

(() => {
basecoin.verifyAddress({
address: 'i1skda3kso43?memoId=243432',
rootAddress: 'ks13kdh245ls',
});
}).should.throw();

(() => {
basecoin.verifyAddress({
address: 'i1skda3kso43=x',
rootAddress: 'i1skda3kso43',
});
}).should.throw();

(() => {
basecoin.verifyAddress({
address: 'i1skda3kso43',
});
}).should.throw();
});

it('isValidMemoId should work', function() {
basecoin.isValidMemo('1').should.equal(true);
basecoin.isValidMemo('uno').should.equal(true);
const string257CharsLong = '4WMNlu0fFU8N94AwukfpfPPQn2Myo80JdmLNF5rgeKAab9XLD93KUQipcT6US0LRwWWIGbUt89fjmdwpg3CBklNi8QIeBI2i8UDJCEuQKYobR5m4ismm1RooTXUnw5OPjmfLuuajYV4e5cS1jpC6hez5X43PZ5SsGaHNYX2YYXY03ir54cWWx5QW5VCPKPKUzfq2UYK5fjAG2Fe3xCUOzqgoR6KaAiuOOnDSyhZygLJyaoJpOXZM9olblNtAW75Ed';
basecoin.isValidMemo(string257CharsLong).should.equal(false);
});

it('should validate pub key', () => {
const { pub } = basecoin.keychains().create();
basecoin.isValidPub(pub).should.equal(true);
});

describe('Keypairs:', () => {
it('should generate a keypair from random seed', function() {
const keyPair = basecoin.generateKeyPair();
keyPair.should.have.property('pub');
keyPair.should.have.property('prv');

basecoin.isValidPub(keyPair.pub).should.equal(true);
basecoin.isValidPrv(keyPair.prv).should.equal(true);
});

it('should generate a keypair from seed', function() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this testing the same thing as the test above, except we do the crypto.randomBytes() before we call the function? If the intention is to make sure we deterministically generate a keypair from a seed, then it might make more sense to use a static seed instead of a random one and make sure you get the key you're expecting.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, it makes more sense. Done.

const seed = Buffer.from('c3b09c24731be2851b641d9d5b3f60fa129695c24071768d15654bea207b7bb6', 'hex');
const keyPair = basecoin.generateKeyPair(seed);

keyPair.pub.should.equal('xpub661MyMwAqRbcF2SUqUMiqxWGwaVX6sH4okTtX8jxJ1A14wfL8W7jZEoNE537JqSESXFpTcXCZahPz7RKQLpAEGsVp233dc5CffLSecpU13X');
keyPair.prv.should.equal('xprv9s21ZrQH143K2YN1jSpiUpZYPYf2hQZDSXYHikLLjfd2C9LBaxoV1SUtNnZGnXeyJ6uFWMbQTfjXqVfgNqRBw5yyaCtBK1AM8PF3XZtKjQp');
});
});
});

Loading