From 851ca7acc9334880cdc8439833578d910f9e7822 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Thu, 13 Sep 2018 18:48:51 -0700 Subject: [PATCH] Catch up to solana 0.8 Transaction wire format changes --- web3.js/src/connection.js | 71 ++++++-------------- web3.js/src/index.js | 1 + web3.js/src/transaction.js | 134 +++++++++++++++++++++++++++++++++++++ 3 files changed, 154 insertions(+), 52 deletions(-) create mode 100644 web3.js/src/transaction.js diff --git a/web3.js/src/connection.js b/web3.js/src/connection.js index 8e6c577030..37fa343de3 100644 --- a/web3.js/src/connection.js +++ b/web3.js/src/connection.js @@ -3,21 +3,11 @@ import assert from 'assert'; 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 {bs58DecodePublicKey, Transaction} from './transaction'; import type {Account, PublicKey} from './account'; - -/** - * @typedef {string} TransactionSignature - */ -export type TransactionSignature = string; - -/** - * @typedef {string} TransactionId - */ -export type TransactionId = string; +import type {TransactionSignature, TransactionId} from './transaction'; type RpcRequest = (methodName: string, args: Array) => any; @@ -228,54 +218,31 @@ export class Connection { return res.result; } + /** * Send tokens to another account */ async sendTokens(from: Account, to: PublicKey, amount: number): Promise { - const lastId = await this.getLastId(); - const fee = 0; + const transaction = new Transaction(); + transaction.fee = 0; + transaction.lastId = await this.getLastId(); + transaction.keys[0] = from.publicKey; + transaction.keys[1] = to; - // - // TODO: Redo this... - // + // Forge a simple Budget Pay contract into `userdata` + // TODO: Clean this up + const userdata = Buffer.alloc(68); // 68 = serialized size of Budget enum + userdata.writeUInt32LE(60, 0); + userdata.writeUInt32LE(amount, 12); // u64 + userdata.writeUInt32LE(amount, 28); // u64 + const toData = bs58DecodePublicKey(to); + toData.copy(userdata, 36); + transaction.userdata = userdata; - // 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); + transaction.sign(from); // Send it + const wireTransaction = transaction.serialize(); const unsafeRes = await this._rpcRequest('sendTransaction', [[...wireTransaction]]); const res = SendTokensRpcResult(unsafeRes); if (res.error) { diff --git a/web3.js/src/index.js b/web3.js/src/index.js index 2a12c49a56..31ad2648be 100644 --- a/web3.js/src/index.js +++ b/web3.js/src/index.js @@ -1,3 +1,4 @@ // @flow export {Account} from './account'; export {Connection} from './connection'; +export {Transaction} from './transaction'; diff --git a/web3.js/src/transaction.js b/web3.js/src/transaction.js new file mode 100644 index 0000000000..18eb2ec7bd --- /dev/null +++ b/web3.js/src/transaction.js @@ -0,0 +1,134 @@ +// @flow + +import assert from 'assert'; +import nacl from 'tweetnacl'; +import bs58 from 'bs58'; + +import type {Account, PublicKey} from './account'; + +/** + * @private + */ +export function bs58DecodePublicKey(key: PublicKey): Buffer { + const keyBytes = Buffer.from(bs58.decode(key)); + assert(keyBytes.length === 32); + return keyBytes; +} + +/** + * @typedef {string} TransactionSignature + */ +export type TransactionSignature = string; + +/** + * @typedef {string} TransactionId + */ +export type TransactionId = string; + +/** + * Mirrors the Transaction struct in src/transaction.rs + */ +export class Transaction { + + /** + * Current signature of the transaction. Typically created by invoking the + * `sign()` method + */ + signature: ?Buffer; + + /** + * Public keys to include in this transaction + */ + keys: Array = []; + + /** + * A recent transaction id. Must be populated by the caller + */ + lastId: ?TransactionId; + + /** + * Fee for this transaction + */ + fee: number = 0; + + /** + * Contract input userdata to include + */ + userdata: Buffer = Buffer.alloc(0); + + /** + * @private + */ + _getSignData(): Buffer { + const {lastId} = this; + if (!lastId) { + throw new Error('Transaction lastId required'); + } + + // Start with a Buffer that should be large enough to fit any Transaction + const transactionData = Buffer.alloc(2048); + + let pos = 0; + + // serialize `this.keys` + transactionData.writeUInt32LE(this.keys.length, pos); // u64 + pos += 8; + for (let key of this.keys) { + const keyBytes = bs58DecodePublicKey(key); + keyBytes.copy(transactionData, pos); + pos += 32; + } + + // serialize `this.lastId` + { + const lastIdBytes = Buffer.from(bs58.decode(lastId)); + assert(lastIdBytes.length === 32); + lastIdBytes.copy(transactionData, pos); + pos += 32; + } + + // serialize `this.fee` + transactionData.writeUInt32LE(this.fee, pos); // u64 + pos += 8; + + // serialize `this.userdata` + if (this.userdata.length > 0) { + this.userdata.copy(transactionData, pos); + pos += this.userdata.length; + } + + return transactionData.slice(0, pos); + } + + /** + * Sign the Transaction with the specified account + * + * The Transaction must be assigned a valid `lastId` before invoking this method + */ + sign(from: Account) { + const transactionData = this._getSignData(); + this.signature = nacl.sign.detached(transactionData, from.secretKey); + assert(this.signature.length === 64); + } + + /** + * Serialize the Transaction in the wire format. + * + * The Transaction must have a valid `signature` before invoking this method + */ + serialize(): Buffer { + const {signature} = this; + if (!signature) { + throw new Error('Transaction has not been signed'); + } + + const transactionData = this._getSignData(); + const wireTransaction = Buffer.alloc( + signature.length + transactionData.length + ); + + Buffer.from(signature).copy(wireTransaction, 0); + transactionData.copy(wireTransaction, signature.length); + return wireTransaction; + } +}