diff --git a/web3.js/examples/account.js b/web3.js/examples/account.js index ca52858315..707bf13c83 100644 --- a/web3.js/examples/account.js +++ b/web3.js/examples/account.js @@ -7,4 +7,4 @@ const solanaWeb3 = require('..'); //const solanaWeb3 = require('@solana/web3.js'); const account = new solanaWeb3.Account(); -console.log(account.publicKey); +console.log(account.publicKey.toString()); diff --git a/web3.js/flow-typed/bn.js.js b/web3.js/flow-typed/bn.js.js new file mode 100644 index 0000000000..5412142593 --- /dev/null +++ b/web3.js/flow-typed/bn.js.js @@ -0,0 +1,4 @@ +declare module 'bn.js' { + // TODO: Fill in types + declare module.exports: any; +} diff --git a/web3.js/module.flow.js b/web3.js/module.flow.js index 921c002653..25397b9749 100644 --- a/web3.js/module.flow.js +++ b/web3.js/module.flow.js @@ -13,11 +13,20 @@ declare module '@solana/web3.js' { declare export type PublicKey = string; + // === src/publickey.js === + declare export class PublicKey { + constructor(number: string | Buffer | Array): PublicKey; + static isPublicKey(o: Object): boolean; + equals(publickey: PublicKey): boolean; + toBase58(): string; + toBuffer(): Buffer; + } + // === src/account.js === declare export class Account { constructor(secretKey: ?Buffer): Account; publicKey: PublicKey; - secretKey: PublicKey; + secretKey: Buffer; } // === src/budget-program.js === @@ -84,6 +93,5 @@ declare module '@solana/web3.js' { constructor(opts?: TransactionCtorFields): Transaction; sign(from: Account): void; serialize(): Buffer; - static serializePublicKey(key: PublicKey): Buffer; } } diff --git a/web3.js/package-lock.json b/web3.js/package-lock.json index afd4e3f82a..6ad1cf257b 100644 --- a/web3.js/package-lock.json +++ b/web3.js/package-lock.json @@ -1,6 +1,6 @@ { "name": "@solana/web3.js", - "version": "0.0.7", + "version": "0.0.0-development", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2261,8 +2261,7 @@ "bn.js": { "version": "4.11.8", "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==", - "dev": true + "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" }, "boolbase": { "version": "1.0.0", diff --git a/web3.js/package.json b/web3.js/package.json index 2667f6e63b..c7d422dc0a 100644 --- a/web3.js/package.json +++ b/web3.js/package.json @@ -43,6 +43,7 @@ }, "dependencies": { "babel-runtime": "^6.26.0", + "bn.js": "^4.11.8", "bs58": "^4.0.1", "jayson": "^2.0.6", "node-fetch": "^2.2.0", diff --git a/web3.js/src/account.js b/web3.js/src/account.js index 1f2a8a4e87..d9405a8ff3 100644 --- a/web3.js/src/account.js +++ b/web3.js/src/account.js @@ -1,14 +1,8 @@ // @flow import nacl from 'tweetnacl'; -import bs58 from 'bs58'; import type {KeyPair} from 'tweetnacl'; -/** - * Base 58 encoded public key - * - * @typedef {string} PublicKey - */ -export type PublicKey = string; +import {PublicKey} from './publickey'; /** * An account key pair (public and secret keys). @@ -36,7 +30,7 @@ export class Account { * The public key for this account */ get publicKey(): PublicKey { - return bs58.encode(this._keypair.publicKey); + return new PublicKey(this._keypair.publicKey); } /** diff --git a/web3.js/src/budget-program.js b/web3.js/src/budget-program.js index 742e677068..4a4707de9e 100644 --- a/web3.js/src/budget-program.js +++ b/web3.js/src/budget-program.js @@ -1,7 +1,7 @@ // @flow import {Transaction} from './transaction'; -import type {PublicKey} from './account'; +import {PublicKey} from './publickey'; /** * Represents a condition that is met by executing a `applySignature()` @@ -54,7 +54,7 @@ export type BudgetCondition = SignatureCondition | TimestampCondition; * @private */ function serializePayment(payment: Payment): Buffer { - const toData = Transaction.serializePublicKey(payment.to); + const toData = payment.to.toBuffer(); const userdata = Buffer.alloc(8 + toData.length); userdata.writeUInt32LE(payment.amount, 0); toData.copy(userdata, 8); @@ -98,7 +98,7 @@ function serializeCondition(condition: BudgetCondition) { case 'timestamp': { const date = serializeDate(condition.when); - const from = Transaction.serializePublicKey(condition.from); + const from = condition.from.toBuffer(); const userdata = Buffer.alloc(4 + date.length + from.length); userdata.writeUInt32LE(0, 0); // Condition enum = Timestamp @@ -108,7 +108,7 @@ function serializeCondition(condition: BudgetCondition) { } case 'signature': { - const from = Transaction.serializePublicKey(condition.from); + const from = condition.from.toBuffer(); const userdata = Buffer.alloc(4 + from.length); userdata.writeUInt32LE(1, 0); // Condition enum = Signature @@ -130,7 +130,7 @@ export class BudgetProgram { * Public key that identifies the Budget program */ static get programId(): PublicKey { - return '4uQeVj5tqViQh7yWWGStvkEG1Zmhx6uasJtWCJziofM'; + return new PublicKey('0x100000000000000000000000000000000000000000000000000000000000000'); } /** diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index fd4bef99eb..af8cf21e3c 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -4,10 +4,10 @@ import assert from 'assert'; import fetch from 'node-fetch'; import jayson from 'jayson/lib/client/browser'; import {struct} from 'superstruct'; -import bs58 from 'bs58'; import {Transaction} from './transaction'; -import type {Account, PublicKey} from './account'; +import {PublicKey} from './publickey'; +import type {Account} from './account'; import type {TransactionSignature, TransactionId} from './transaction'; type RpcRequest = (methodName: string, args: Array) => any; @@ -174,7 +174,7 @@ export class Connection { async getBalance(publicKey: PublicKey): Promise { const unsafeRes = await this._rpcRequest( 'getBalance', - [publicKey] + [publicKey.toBase58()] ); const res = GetBalanceRpcResult(unsafeRes); if (res.error) { @@ -190,7 +190,7 @@ export class Connection { async getAccountInfo(publicKey: PublicKey): Promise { const unsafeRes = await this._rpcRequest( 'getAccountInfo', - [publicKey] + [publicKey.toBase58()] ); const res = GetAccountInfoRpcResult(unsafeRes); if (res.error) { @@ -202,7 +202,7 @@ export class Connection { return { tokens: result.tokens, - programId: bs58.encode(result.program_id), + programId: new PublicKey(result.program_id), userdata: Buffer.from(result.userdata), }; } @@ -280,7 +280,7 @@ export class Connection { * Request an allocation of tokens to the specified account */ async requestAirdrop(to: PublicKey, amount: number): Promise { - const unsafeRes = await this._rpcRequest('requestAirdrop', [to, amount]); + const unsafeRes = await this._rpcRequest('requestAirdrop', [to.toBase58(), amount]); const res = RequestAirdropRpcResult(unsafeRes); if (res.error) { throw new Error(res.error.message); diff --git a/web3.js/src/index.js b/web3.js/src/index.js index f613a2d68d..506a72cc38 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -1,6 +1,7 @@ // @flow export {Account} from './account'; -export {Connection} from './connection'; -export {Transaction} from './transaction'; -export {SystemProgram} from './system-program'; export {BudgetProgram} from './budget-program'; +export {Connection} from './connection'; +export {PublicKey} from './publickey'; +export {SystemProgram} from './system-program'; +export {Transaction} from './transaction'; diff --git a/web3.js/src/publickey.js b/web3.js/src/publickey.js new file mode 100644 index 0000000000..9af5f9c499 --- /dev/null +++ b/web3.js/src/publickey.js @@ -0,0 +1,70 @@ +// @flow + +import BN from 'bn.js'; +import bs58 from 'bs58'; + +/** + * A public key + */ +export class PublicKey { + _bn: BN; + + /** + * Create a new PublicKey object + */ + constructor(number: string | Buffer | Array) { + let radix = 10; + + if (typeof number === 'string' && number.startsWith('0x')) { + this._bn = new BN(number.substring(2), 16); + } else { + this._bn = new BN(number, radix); + } + if (this._bn.byteLength() > 32) { + throw new Error(`Invalid public key input`); + } + } + + /** + * Checks if the provided object is a PublicKey + */ + static isPublicKey(o: Object): boolean { + return o instanceof PublicKey; + } + + /** + * Checks if two publicKeys are equal + */ + equals(publicKey: PublicKey): boolean { + return this._bn.eq(publicKey._bn); + } + + /** + * Return the base-58 representation of the public key + */ + toBase58(): string { + return bs58.encode(this.toBuffer()); + } + + /** + * Return the base-58 representation of the public key + */ + toBuffer(): Buffer { + const b = this._bn.toBuffer(); + if (b.length === 32) { + return b; + } + + const zeroPad = new Buffer(32); + b.copy(zeroPad, 32 - b.length); + return zeroPad; + } + + /** + * Returns a string representation of the public key + */ + toString(): string { + return this.toBase58(); + } +} + diff --git a/web3.js/src/system-program.js b/web3.js/src/system-program.js index 2804b7f036..8029e83009 100644 --- a/web3.js/src/system-program.js +++ b/web3.js/src/system-program.js @@ -3,7 +3,7 @@ import assert from 'assert'; import {Transaction} from './transaction'; -import type {PublicKey} from './account'; +import {PublicKey} from './publickey'; /** * Factory class for transactions to interact with the System program @@ -13,7 +13,7 @@ export class SystemProgram { * Public key that identifies the System program */ static get programId(): PublicKey { - return '11111111111111111111111111111111'; + return new PublicKey('0x000000000000000000000000000000000000000000000000000000000000000'); } /** @@ -38,7 +38,7 @@ export class SystemProgram { userdata.writeUInt32LE(space, pos); // space as u64 pos += 8; - const programIdBytes = Transaction.serializePublicKey(programId); + const programIdBytes = programId.toBuffer(); programIdBytes.copy(userdata, pos); pos += 32; @@ -84,7 +84,7 @@ export class SystemProgram { userdata.writeUInt32LE(1, pos); // Assign instruction pos += 4; - const programIdBytes = Transaction.serializePublicKey(programId); + const programIdBytes = programId.toBuffer(); programIdBytes.copy(userdata, pos); pos += programIdBytes.length; diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js index 23fc39080b..8aec617998 100644 --- a/web3.js/src/transaction.js +++ b/web3.js/src/transaction.js @@ -4,7 +4,8 @@ import assert from 'assert'; import nacl from 'tweetnacl'; import bs58 from 'bs58'; -import type {Account, PublicKey} from './account'; +import type {Account} from './account'; +import type {PublicKey} from './publickey'; /** * @typedef {string} TransactionSignature @@ -92,14 +93,14 @@ export class Transaction { transactionData.writeUInt32LE(this.keys.length, pos); // u64 pos += 8; for (let key of this.keys) { - const keyBytes = Transaction.serializePublicKey(key); + const keyBytes = key.toBuffer(); keyBytes.copy(transactionData, pos); pos += 32; } // serialize `this.programId` if (this.programId) { - const keyBytes = Transaction.serializePublicKey(this.programId); + const keyBytes = this.programId.toBuffer(); keyBytes.copy(transactionData, pos); } pos += 32; @@ -158,14 +159,5 @@ export class Transaction { transactionData.copy(wireTransaction, signature.length); return wireTransaction; } - - /** - * Serializes a public key into the wire format - */ - static serializePublicKey(key: PublicKey): Buffer { - const data = Buffer.from(bs58.decode(key)); - assert(data.length === 32); - return data; - } } diff --git a/web3.js/test/account.test.js b/web3.js/test/account.test.js index 8c18317081..341c085f79 100644 --- a/web3.js/test/account.test.js +++ b/web3.js/test/account.test.js @@ -1,10 +1,10 @@ // @flow import {Account} from '../src/account'; +import {PublicKey} from '../src/publickey'; test('generate new account', () => { const account = new Account(); - expect(account.publicKey.length).toBeGreaterThanOrEqual(43); - expect(account.publicKey.length).toBeLessThanOrEqual(44); + expect(PublicKey.isPublicKey(account.publicKey)).toBeTruthy(); expect(account.secretKey).toHaveLength(64); }); @@ -17,5 +17,5 @@ test('account from secret key', () => { 74, 101, 217, 139, 135, 139, 153, 34 ]); const account = new Account(secretKey); - expect(account.publicKey).toBe('2q7pyhPwAwZ3QMfZrnAbDhnh9mDUqycszcpf86VgQxhF'); + expect(account.publicKey.toBase58()).toBe('2q7pyhPwAwZ3QMfZrnAbDhnh9mDUqycszcpf86VgQxhF'); }); diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 8389159e81..4cc20f373a 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -26,7 +26,7 @@ test('get account info - error', () => { url, { method: 'getAccountInfo', - params: [account.publicKey], + params: [account.publicKey.toBase58()], }, errorResponse, ]); @@ -44,7 +44,7 @@ test('get balance', async () => { url, { method: 'getBalance', - params: [account.publicKey], + params: [account.publicKey.toBase58()], }, { error: null, @@ -56,23 +56,6 @@ test('get balance', async () => { expect(balance).toBeGreaterThanOrEqual(0); }); -test('get balance - error', () => { - const connection = new Connection(url); - - const invalidPublicKey = 'not a public key'; - mockRpc.push([ - url, - { - method: 'getBalance', - params: [invalidPublicKey], - }, - errorResponse, - ]); - - expect(connection.getBalance(invalidPublicKey)) - .rejects.toThrow(errorMessage); -}); - test('confirm transaction - error', () => { const connection = new Connection(url); @@ -174,7 +157,7 @@ test('request airdrop', async () => { url, { method: 'requestAirdrop', - params: [account.publicKey, 40], + params: [account.publicKey.toBase58(), 40], }, { error: null, @@ -185,7 +168,7 @@ test('request airdrop', async () => { url, { method: 'requestAirdrop', - params: [account.publicKey, 2], + params: [account.publicKey.toBase58(), 2], }, { error: null, @@ -196,7 +179,7 @@ test('request airdrop', async () => { url, { method: 'getBalance', - params: [account.publicKey], + params: [account.publicKey.toBase58()], }, { error: null, @@ -214,7 +197,7 @@ test('request airdrop', async () => { url, { method: 'getAccountInfo', - params: [account.publicKey], + params: [account.publicKey.toBase58()], }, { error: null, @@ -226,31 +209,13 @@ test('request airdrop', async () => { tokens: 42, userdata: [], } - } ]); const accountInfo = await connection.getAccountInfo(account.publicKey); expect(accountInfo.tokens).toBe(42); expect(accountInfo.userdata).toHaveLength(0); - expect(accountInfo.programId).toBe(SystemProgram.programId); -}); - -test('request airdrop - error', () => { - const invalidPublicKey = 'not a public key'; - const connection = new Connection(url); - - mockRpc.push([ - url, - { - method: 'requestAirdrop', - params: [invalidPublicKey, 1], - }, - errorResponse - ]); - - expect(connection.requestAirdrop(invalidPublicKey, 1)) - .rejects.toThrow(errorMessage); + expect(accountInfo.programId).toEqual(SystemProgram.programId); }); test('transaction', async () => { @@ -262,7 +227,7 @@ test('transaction', async () => { url, { method: 'requestAirdrop', - params: [accountFrom.publicKey, 12], + params: [accountFrom.publicKey.toBase58(), 12], }, { error: null, @@ -273,7 +238,7 @@ test('transaction', async () => { url, { method: 'getBalance', - params: [accountFrom.publicKey], + params: [accountFrom.publicKey.toBase58()], }, { error: null, @@ -287,7 +252,7 @@ test('transaction', async () => { url, { method: 'requestAirdrop', - params: [accountTo.publicKey, 21], + params: [accountTo.publicKey.toBase58(), 21], }, { error: null, @@ -298,7 +263,7 @@ test('transaction', async () => { url, { method: 'getBalance', - params: [accountTo.publicKey], + params: [accountTo.publicKey.toBase58()], }, { error: null, @@ -375,7 +340,7 @@ test('transaction', async () => { url, { method: 'getBalance', - params: [accountFrom.publicKey], + params: [accountFrom.publicKey.toBase58()], }, { error: null, @@ -388,7 +353,7 @@ test('transaction', async () => { url, { method: 'getBalance', - params: [accountTo.publicKey], + params: [accountTo.publicKey.toBase58()], }, { error: null, diff --git a/web3.js/test/publickey.test.js b/web3.js/test/publickey.test.js new file mode 100644 index 0000000000..dd1d5f7732 --- /dev/null +++ b/web3.js/test/publickey.test.js @@ -0,0 +1,51 @@ +// @flow +import {PublicKey} from '../src/publickey'; + +test('invalid', () => { + expect(() => { + new PublicKey([ + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]); + }).toThrow(); + + expect(() => { + new PublicKey('0x300000000000000000000000000000000000000000000000000000000000000000000'); + }).toThrow(); + + expect(() => { + new PublicKey('135693854574979916511997248057056142015550763280047535983739356259273198796800000'); + }).toThrow(); +}); + +test('equals', () => { + const arrayKey = new PublicKey([ + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]); + const hexKey = new PublicKey('0x300000000000000000000000000000000000000000000000000000000000000'); + const decimalKey = new PublicKey('1356938545749799165119972480570561420155507632800475359837393562592731987968'); + + expect(arrayKey.equals(hexKey)).toBeTruthy(); + expect(arrayKey.equals(decimalKey)).toBeTruthy(); +}); + +test('isPublicKey', () => { + const key = new PublicKey('0x100000000000000000000000000000000000000000000000000000000000000'); + expect(PublicKey.isPublicKey(key)).toBeTruthy(); + expect(PublicKey.isPublicKey({})).toBeFalsy(); +}); + +test('toBase58', () => { + const key = new PublicKey('0x300000000000000000000000000000000000000000000000000000000000000'); + expect(key.toBase58()).toBe('CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3'); +}); + +test('toBuffer', () => { + const key = new PublicKey('0x300000000000000000000000000000000000000000000000000000000000000'); + expect(key.toBuffer()).toHaveLength(32); + expect(key.toBase58()).toBe('CiDwVBFgWV9E5MvXWoLgnEgn2hK7rJikbvfWavzAQz3'); + + const key2 = new PublicKey('0x000000000000000000000000000000000000000000000000000000000000000'); + expect(key2.toBuffer()).toHaveLength(32); + expect(key2.toBase58()).toBe('11111111111111111111111111111111'); +}); +