diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 4d8a5de522..0b014660f4 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -5,6 +5,7 @@ import fetch from 'node-fetch'; import jayson from 'jayson/lib/client/browser'; import nacl from 'tweetnacl'; import {struct} from 'superstruct'; +import bs58 from 'bs58'; import type {Account, PublicKey} from './account'; @@ -229,19 +230,53 @@ export class Connection { /** * Send tokens to another account - * - * @todo THIS METHOD IS NOT FULLY IMPLEMENTED YET - * @ignore */ async sendTokens(from: Account, to: PublicKey, amount: number): Promise { - const transaction = Buffer.from( - // TODO: This is not the correct transaction payload - `Transaction ${from.publicKey} ${to} ${amount}` - ); + const lastId = await this.getLastId(); + const fee = 0; - const signedTransaction = nacl.sign.detached(transaction, from.secretKey); + // + // TODO: Redo this... + // - const unsafeRes = await this._rpcRequest('sendTransaction', [[...signedTransaction]]); + // Build the transaction data to be signed. + const transactionData = Buffer.alloc(124); + transactionData.writeUInt32LE(amount, 4); // u64 + transactionData.writeUInt32LE(amount - fee, 20); // u64 + transactionData.writeUInt32LE(32, 28); // length of public key (u64) + { + const toBytes = Buffer.from(bs58.decode(to)); + assert(toBytes.length === 32); + toBytes.copy(transactionData, 36); + } + + transactionData.writeUInt32LE(32, 68); // length of last id (u64) + { + const lastIdBytes = Buffer.from(bs58.decode(lastId)); + assert(lastIdBytes.length === 32); + lastIdBytes.copy(transactionData, 76); + } + + // Sign it + const signature = nacl.sign.detached(transactionData, from.secretKey); + assert(signature.length === 64); + + // Build the over-the-wire transaction buffer + const wireTransaction = Buffer.alloc(236); + wireTransaction.writeUInt32LE(64, 0); // signature length (u64) + Buffer.from(signature).copy(wireTransaction, 8); + + + wireTransaction.writeUInt32LE(32, 72); // public key length (u64) + { + const fromBytes = Buffer.from(bs58.decode(from.publicKey)); + assert(fromBytes.length === 32); + fromBytes.copy(wireTransaction, 80); + } + transactionData.copy(wireTransaction, 112); + + // Send it + const unsafeRes = await this._rpcRequest('sendTransaction', [[...wireTransaction]]); const res = SendTokensRpcResult(unsafeRes); if (res.error) { throw new Error(res.error.message); @@ -251,4 +286,3 @@ export class Connection { return res.result; } } - diff --git a/web3.js/test/__mocks__/node-fetch.js b/web3.js/test/__mocks__/node-fetch.js index b98ce78289..6278983aea 100644 --- a/web3.js/test/__mocks__/node-fetch.js +++ b/web3.js/test/__mocks__/node-fetch.js @@ -1,8 +1,10 @@ // @flow +import fetch from 'node-fetch'; + type RpcRequest = { method: string; - params: Array; + params?: Array; }; type RpcResponseError = { @@ -20,6 +22,13 @@ export const mockRpc: Array<[string, RpcRequest, RpcResponse]> = []; // eslint-disable-next-line no-undef const mock: JestMockFn = jest.fn( (fetchUrl, fetchOptions) => { + // Define DOITLIVE in the environment to test against the real full node + // identified by `url` instead of using the mock + if (process.env.DOITLIVE) { + console.log(`Note: node-fetch mock is disabled, testing live against ${fetchUrl}`); + return fetch(fetchUrl, fetchOptions); + } + expect(mockRpc.length).toBeGreaterThanOrEqual(1); const [mockUrl, mockRequest, mockResponse] = mockRpc.shift(); @@ -38,7 +47,6 @@ const mock: JestMockFn = jest.fn( { jsonrpc: '2.0', method: 'invalid', - params: ['invalid', 'params'], }, mockRequest )); diff --git a/web3.js/test/connection.test.js b/web3.js/test/connection.test.js index 1bce6e1cfb..4b96603fe0 100644 --- a/web3.js/test/connection.test.js +++ b/web3.js/test/connection.test.js @@ -5,14 +5,7 @@ import {Account} from '../src/account'; import {mockRpc} from './__mocks__/node-fetch'; const url = 'http://master.testnet.solana.com:8899'; - -// Define DOITLIVE in the environment to test against the real full node -// identified by `url` instead of using the mock -if (process.env.DOITLIVE) { - console.log(`Note: node-fetch mock is disabled, testing live against ${url}`); -} else { - jest.mock('node-fetch'); -} +//const url = 'http://localhost:8899'; const errorMessage = 'Invalid request'; const errorResponse = { @@ -22,6 +15,7 @@ const errorResponse = { result: undefined, }; + test('get balance', async () => { const account = new Account(); const connection = new Connection(url); @@ -109,7 +103,7 @@ test('get last Id', async () => { }, { error: null, - result: '1111111111111111111111111111111111111111111111', + result: '2BjEqiiT43J6XskiHdz7aoocjPeWkCPiKD72SiFQsrA2', } ] ); @@ -200,35 +194,125 @@ test('request airdrop - error', () => { .rejects.toThrow(errorMessage); }); -test('send transaction - error', () => { - const secretKey = Buffer.from([ - 153, 218, 149, 89, 225, 94, 145, 62, 233, 171, 46, 83, 227, - 223, 173, 87, 93, 163, 59, 73, 190, 17, 37, 187, 146, 46, 51, - 73, 79, 73, 136, 40, 27, 47, 73, 9, 110, 62, 93, 189, 15, 207, - 169, 192, 192, 205, 146, 217, 171, 59, 33, 84, 75, 52, 213, 221, - 74, 101, 217, 139, 135, 139, 153, 34 - ]); - const account = new Account(secretKey); +test('transaction', async () => { + const accountFrom = new Account(); + const accountTo = new Account(); const connection = new Connection(url); mockRpc.push([ url, { - method: 'sendTransaction', - params: [[ - 78, 52, 48, 146, 162, 213, 83, 169, 128, 10, 82, 26, 145, 238, - 1, 130, 16, 44, 249, 99, 121, 55, 217, 72, 77, 41, 73, 227, 5, - 15, 125, 212, 186, 157, 182, 100, 232, 232, 39, 84, 5, 121, 172, - 137, 177, 248, 188, 224, 196, 102, 204, 43, 128, 243, 170, 157, - 134, 216, 209, 8, 211, 209, 44, 1 - ]], + method: 'requestAirdrop', + params: [accountFrom.publicKey, 12], }, - errorResponse, + { + error: null, + result: true, + } ]); + mockRpc.push([ + url, + { + method: 'getBalance', + params: [accountFrom.publicKey], + }, + { + error: null, + result: 12, + } + ]); + await connection.requestAirdrop(accountFrom.publicKey, 12); + expect(await connection.getBalance(accountFrom.publicKey)).toBe(12); + mockRpc.push([ + url, + { + method: 'requestAirdrop', + params: [accountTo.publicKey, 21], + }, + { + error: null, + result: true, + } + ]); + mockRpc.push([ + url, + { + method: 'getBalance', + params: [accountTo.publicKey], + }, + { + error: null, + result: 21, + } + ]); + await connection.requestAirdrop(accountTo.publicKey, 21); + expect(await connection.getBalance(accountTo.publicKey)).toBe(21); - expect(connection.sendTokens(account, account.publicKey, 123)) - .rejects.toThrow(errorMessage); + mockRpc.push([ + url, + { + method: 'getLastId', + params: [], + }, + { + error: null, + result: '2BjEqiiT43J6XskiHdz7aoocjPeWkCPiKD72SiFQsrA2', + } + ] + ); + mockRpc.push([ + url, + { + method: 'sendTransaction', + }, + { + error: null, + result: '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk', + } + ] + ); + const signature = await connection.sendTokens(accountFrom, accountTo.publicKey, 10); + + mockRpc.push([ + url, + { + method: 'confirmTransaction', + params: [ + '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk' + ], + }, + { + error: null, + result: true, + } + ] + ); + expect(connection.confirmTransaction(signature)).resolves.toBe(true); + + mockRpc.push([ + url, + { + method: 'getBalance', + params: [accountFrom.publicKey], + }, + { + error: null, + result: 2, + } + ]); + expect(await connection.getBalance(accountFrom.publicKey)).toBe(2); + + mockRpc.push([ + url, + { + method: 'getBalance', + params: [accountTo.publicKey], + }, + { + error: null, + result: 31, + } + ]); + expect(await connection.getBalance(accountTo.publicKey)).toBe(31); }); - -