-
Notifications
You must be signed in to change notification settings - Fork 305
Add EOS address methods to SDK #395
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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) { | ||
| 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}`); | ||
| } | ||
| } | ||
| } | ||
| 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'; | ||
| } | ||
| } |
| 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() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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'); | ||
| }); | ||
| }); | ||
| }); | ||
|
|
||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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