Skip to content

Commit 5111c2b

Browse files
committed
Add preliminary sendTokens() implementation
1 parent d15ee1a commit 5111c2b

File tree

3 files changed

+168
-42
lines changed

3 files changed

+168
-42
lines changed

src/connection.js

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import fetch from 'node-fetch';
55
import jayson from 'jayson/lib/client/browser';
66
import nacl from 'tweetnacl';
77
import {struct} from 'superstruct';
8+
import bs58 from 'bs58';
89

910
import type {Account, PublicKey} from './account';
1011

@@ -229,19 +230,53 @@ export class Connection {
229230

230231
/**
231232
* Send tokens to another account
232-
*
233-
* @todo THIS METHOD IS NOT FULLY IMPLEMENTED YET
234-
* @ignore
235233
*/
236234
async sendTokens(from: Account, to: PublicKey, amount: number): Promise<TransactionSignature> {
237-
const transaction = Buffer.from(
238-
// TODO: This is not the correct transaction payload
239-
`Transaction ${from.publicKey} ${to} ${amount}`
240-
);
235+
const lastId = await this.getLastId();
236+
const fee = 0;
237+
238+
//
239+
// TODO: Redo this...
240+
//
241+
242+
// Build the transaction data to be signed.
243+
const transactionData = Buffer.alloc(124);
244+
transactionData.writeUInt32LE(amount, 4); // u64
245+
transactionData.writeUInt32LE(amount - fee, 20); // u64
246+
transactionData.writeUInt32LE(32, 28); // length of public key (u64)
247+
{
248+
const toBytes = Buffer.from(bs58.decode(to));
249+
assert(toBytes.length === 32);
250+
toBytes.copy(transactionData, 36);
251+
}
252+
253+
transactionData.writeUInt32LE(32, 68); // length of last id (u64)
254+
{
255+
const lastIdBytes = Buffer.from(bs58.decode(lastId));
256+
assert(lastIdBytes.length === 32);
257+
lastIdBytes.copy(transactionData, 76);
258+
}
259+
260+
// Sign it
261+
const signature = nacl.sign.detached(transactionData, from.secretKey);
262+
assert(signature.length === 64);
263+
264+
// Build the over-the-wire transaction buffer
265+
const wireTransaction = Buffer.alloc(236);
266+
wireTransaction.writeUInt32LE(64, 0); // signature length (u64)
267+
Buffer.from(signature).copy(wireTransaction, 8);
241268

242-
const signedTransaction = nacl.sign.detached(transaction, from.secretKey);
243269

244-
const unsafeRes = await this._rpcRequest('sendTransaction', [[...signedTransaction]]);
270+
wireTransaction.writeUInt32LE(32, 72); // public key length (u64)
271+
{
272+
const fromBytes = Buffer.from(bs58.decode(from.publicKey));
273+
assert(fromBytes.length === 32);
274+
fromBytes.copy(wireTransaction, 80);
275+
}
276+
transactionData.copy(wireTransaction, 112);
277+
278+
// Send it
279+
const unsafeRes = await this._rpcRequest('sendTransaction', [[...wireTransaction]]);
245280
const res = SendTokensRpcResult(unsafeRes);
246281
if (res.error) {
247282
throw new Error(res.error.message);
@@ -251,4 +286,3 @@ export class Connection {
251286
return res.result;
252287
}
253288
}
254-

test/__mocks__/node-fetch.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// @flow
22

3+
import fetch from 'node-fetch';
4+
35
type RpcRequest = {
46
method: string;
5-
params: Array<any>;
7+
params?: Array<any>;
68
};
79

810
type RpcResponseError = {
@@ -20,6 +22,13 @@ export const mockRpc: Array<[string, RpcRequest, RpcResponse]> = [];
2022
// eslint-disable-next-line no-undef
2123
const mock: JestMockFn<any, any> = jest.fn(
2224
(fetchUrl, fetchOptions) => {
25+
// Define DOITLIVE in the environment to test against the real full node
26+
// identified by `url` instead of using the mock
27+
if (process.env.DOITLIVE) {
28+
console.log(`Note: node-fetch mock is disabled, testing live against ${fetchUrl}`);
29+
return fetch(fetchUrl, fetchOptions);
30+
}
31+
2332
expect(mockRpc.length).toBeGreaterThanOrEqual(1);
2433
const [mockUrl, mockRequest, mockResponse] = mockRpc.shift();
2534

@@ -38,7 +47,6 @@ const mock: JestMockFn<any, any> = jest.fn(
3847
{
3948
jsonrpc: '2.0',
4049
method: 'invalid',
41-
params: ['invalid', 'params'],
4250
},
4351
mockRequest
4452
));

test/connection.test.js

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,7 @@ import {Account} from '../src/account';
55
import {mockRpc} from './__mocks__/node-fetch';
66

77
const url = 'http://master.testnet.solana.com:8899';
8-
9-
// Define DOITLIVE in the environment to test against the real full node
10-
// identified by `url` instead of using the mock
11-
if (process.env.DOITLIVE) {
12-
console.log(`Note: node-fetch mock is disabled, testing live against ${url}`);
13-
} else {
14-
jest.mock('node-fetch');
15-
}
8+
//const url = 'http://localhost:8899';
169

1710
const errorMessage = 'Invalid request';
1811
const errorResponse = {
@@ -22,6 +15,7 @@ const errorResponse = {
2215
result: undefined,
2316
};
2417

18+
2519
test('get balance', async () => {
2620
const account = new Account();
2721
const connection = new Connection(url);
@@ -109,7 +103,7 @@ test('get last Id', async () => {
109103
},
110104
{
111105
error: null,
112-
result: '1111111111111111111111111111111111111111111111',
106+
result: '2BjEqiiT43J6XskiHdz7aoocjPeWkCPiKD72SiFQsrA2',
113107
}
114108
]
115109
);
@@ -200,35 +194,125 @@ test('request airdrop - error', () => {
200194
.rejects.toThrow(errorMessage);
201195
});
202196

203-
test('send transaction - error', () => {
204-
const secretKey = Buffer.from([
205-
153, 218, 149, 89, 225, 94, 145, 62, 233, 171, 46, 83, 227,
206-
223, 173, 87, 93, 163, 59, 73, 190, 17, 37, 187, 146, 46, 51,
207-
73, 79, 73, 136, 40, 27, 47, 73, 9, 110, 62, 93, 189, 15, 207,
208-
169, 192, 192, 205, 146, 217, 171, 59, 33, 84, 75, 52, 213, 221,
209-
74, 101, 217, 139, 135, 139, 153, 34
210-
]);
211-
const account = new Account(secretKey);
197+
test('transaction', async () => {
198+
const accountFrom = new Account();
199+
const accountTo = new Account();
212200
const connection = new Connection(url);
213201

214202
mockRpc.push([
215203
url,
216204
{
217-
method: 'sendTransaction',
218-
params: [[
219-
78, 52, 48, 146, 162, 213, 83, 169, 128, 10, 82, 26, 145, 238,
220-
1, 130, 16, 44, 249, 99, 121, 55, 217, 72, 77, 41, 73, 227, 5,
221-
15, 125, 212, 186, 157, 182, 100, 232, 232, 39, 84, 5, 121, 172,
222-
137, 177, 248, 188, 224, 196, 102, 204, 43, 128, 243, 170, 157,
223-
134, 216, 209, 8, 211, 209, 44, 1
224-
]],
205+
method: 'requestAirdrop',
206+
params: [accountFrom.publicKey, 12],
225207
},
226-
errorResponse,
208+
{
209+
error: null,
210+
result: true,
211+
}
227212
]);
213+
mockRpc.push([
214+
url,
215+
{
216+
method: 'getBalance',
217+
params: [accountFrom.publicKey],
218+
},
219+
{
220+
error: null,
221+
result: 12,
222+
}
223+
]);
224+
await connection.requestAirdrop(accountFrom.publicKey, 12);
225+
expect(await connection.getBalance(accountFrom.publicKey)).toBe(12);
228226

227+
mockRpc.push([
228+
url,
229+
{
230+
method: 'requestAirdrop',
231+
params: [accountTo.publicKey, 21],
232+
},
233+
{
234+
error: null,
235+
result: true,
236+
}
237+
]);
238+
mockRpc.push([
239+
url,
240+
{
241+
method: 'getBalance',
242+
params: [accountTo.publicKey],
243+
},
244+
{
245+
error: null,
246+
result: 21,
247+
}
248+
]);
249+
await connection.requestAirdrop(accountTo.publicKey, 21);
250+
expect(await connection.getBalance(accountTo.publicKey)).toBe(21);
229251

230-
expect(connection.sendTokens(account, account.publicKey, 123))
231-
.rejects.toThrow(errorMessage);
232-
});
252+
mockRpc.push([
253+
url,
254+
{
255+
method: 'getLastId',
256+
params: [],
257+
},
258+
{
259+
error: null,
260+
result: '2BjEqiiT43J6XskiHdz7aoocjPeWkCPiKD72SiFQsrA2',
261+
}
262+
]
263+
);
264+
mockRpc.push([
265+
url,
266+
{
267+
method: 'sendTransaction',
268+
},
269+
{
270+
error: null,
271+
result: '3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk',
272+
}
273+
]
274+
);
275+
const signature = await connection.sendTokens(accountFrom, accountTo.publicKey, 10);
276+
277+
mockRpc.push([
278+
url,
279+
{
280+
method: 'confirmTransaction',
281+
params: [
282+
'3WE5w4B7v59x6qjyC4FbG2FEKYKQfvsJwqSxNVmtMjT8TQ31hsZieDHcSgqzxiAoTL56n2w5TncjqEKjLhtF4Vk'
283+
],
284+
},
285+
{
286+
error: null,
287+
result: true,
288+
}
289+
]
290+
);
291+
expect(connection.confirmTransaction(signature)).resolves.toBe(true);
233292

293+
mockRpc.push([
294+
url,
295+
{
296+
method: 'getBalance',
297+
params: [accountFrom.publicKey],
298+
},
299+
{
300+
error: null,
301+
result: 2,
302+
}
303+
]);
304+
expect(await connection.getBalance(accountFrom.publicKey)).toBe(2);
234305

306+
mockRpc.push([
307+
url,
308+
{
309+
method: 'getBalance',
310+
params: [accountTo.publicKey],
311+
},
312+
{
313+
error: null,
314+
result: 31,
315+
}
316+
]);
317+
expect(await connection.getBalance(accountTo.publicKey)).toBe(31);
318+
});

0 commit comments

Comments
 (0)