Skip to content

Commit

Permalink
Add transfer coins and Fix wallet generation logic (#13)
Browse files Browse the repository at this point in the history
* add transferCoins

* update transfer coin readme & test case

* Fix wallet generation code
  • Loading branch information
温深君 authored and perfectmak committed Jul 6, 2019
1 parent f749962 commit a5d1587
Show file tree
Hide file tree
Showing 12 changed files with 163 additions and 70 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ build
node_modules
.DS_Store
.idea
*.iml
17 changes: 15 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,21 @@ await main();

### Transferring Libra Coins

You will eventually be able to transfer libra coins from your account to another account using `client.transferCoins()` function, but it is still a work in progress.
You are welcome to help contribute to making this work.
```javascript
async function main() {
const client = new LibraClient({ network: LibraNetwork.Testnet });
const wallet = new LibraWallet({
mnemonic:
'lend arm arm addict trust release grid unlock exhibit surround deliver front link bean night dry tuna pledge expect net ankle process mammal great',

});
const account = wallet.newAccount();
const account2Address = '854563c50d20788fb6c11fac1010b553d722edb0c02f87c2edbdd3923726d13f';
await client.transferCoins(account, account2Address, 1e6);
}

await main();
```

### Executing Transactions with Custom Program
You will eventually be able to transfer libra coins from your account to another account using `client.execute()` function, but it is also still a work in progress.
Expand Down
31 changes: 28 additions & 3 deletions lib/Transactions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import BigNumber from 'bignumber.js';
import {TransactionArgument} from "./__generated__/transaction_pb";
import Addresses from "./constants/Addresses";
import ProgamBase64Codes from "./constants/ProgamBase64Codes";
import { AccountAddress } from './wallet/Accounts';

interface LibraProgram {
Expand All @@ -8,7 +11,7 @@ interface LibraProgram {
}

interface LibraProgramArgument {
type: string;
type: TransactionArgument.ArgType;
value: Uint8Array;
}

Expand All @@ -18,8 +21,30 @@ interface LibraGasConstraint {
}

export class LibraTransaction {
public static createTransfer(receipientAddress: string, numAccount: BigNumber): LibraTransaction {
throw new Error('Method not implemented. Still working on compiling and encoding programs');
public static createTransfer(recipientAddress: string, numAccount: BigNumber): LibraTransaction {
const amountBuffer = Buffer.alloc(8);
amountBuffer.writeBigUInt64LE(BigInt(numAccount),0);
const programArguments: LibraProgramArgument[] = [
{
type: TransactionArgument.ArgType.ADDRESS,
value: Uint8Array.from(Buffer.from(recipientAddress, 'hex'))
},{
type: TransactionArgument.ArgType.U64,
value: Uint8Array.from(amountBuffer)
}
];
const program: LibraProgram = {
arguments: programArguments,
code: Uint8Array.from(Buffer.from(ProgamBase64Codes.peerToPeerTxn, 'base64')),
modules: []
};
return new LibraTransaction(program, {
gasUnitPrice: new BigNumber(0),
maxGasAmount: new BigNumber(1000000)
},
`${Math.floor(new Date().getTime()/1000) + 100}`,
new Uint8Array(Addresses.AddressLength),
'-0');
}
public program: LibraProgram;
public gasContraint: LibraGasConstraint;
Expand Down
32 changes: 16 additions & 16 deletions lib/__generated__/transaction_pb.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 40 additions & 29 deletions lib/client.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
import axios from 'axios';
import BigNumber from 'bignumber.js';
import { credentials, ServiceError } from 'grpc';
import {credentials, ServiceError} from 'grpc';

import { AccountStateBlob, AccountStateWithProof } from './__generated__/account_state_blob_pb';
import { AdmissionControlClient } from './__generated__/admission_control_grpc_pb';
import { SubmitTransactionRequest, SubmitTransactionResponse } from './__generated__/admission_control_pb';
import SHA3 from "sha3";
import {AccountStateBlob, AccountStateWithProof} from './__generated__/account_state_blob_pb';
import {AdmissionControlClient} from './__generated__/admission_control_grpc_pb';
import {SubmitTransactionRequest, SubmitTransactionResponse} from './__generated__/admission_control_pb';
import {
GetAccountStateRequest,
GetAccountStateResponse,
RequestItem,
ResponseItem,
UpdateToLatestLedgerRequest,
} from './__generated__/get_with_proof_pb';
import { Program, RawTransaction, SignedTransaction, TransactionArgument } from './__generated__/transaction_pb';
import { CursorBuffer } from './common/CursorBuffer';
import {Program, RawTransaction, SignedTransaction, TransactionArgument} from './__generated__/transaction_pb';
import {CursorBuffer} from './common/CursorBuffer';
import HashSaltValues from './constants/HashSaltValues';
import PathValues from './constants/PathValues';
import { LibraTransaction } from './Transactions';
import { Account, AccountAddress, AccountState, AccountStates } from './wallet/Accounts';
import {LibraTransaction} from './Transactions';
import {Account, AccountAddress, AccountState, AccountStates} from './wallet/Accounts';

const DefaultFaucetServerHost = 'faucet.testnet.libra.org';
const DefaultTestnetServerHost = 'ac.testnet.libra.org';
Expand Down Expand Up @@ -100,8 +102,7 @@ export class LibraClient {
if (stateWithProof.hasBlob()) {
const stateBlob = stateWithProof.getBlob() as AccountStateBlob;
const blob = stateBlob.getBlob_asU8();
const accountState = this._decodeAccountStateBlob(blob);
return accountState;
return this._decodeAccountStateBlob(blob);
}

return AccountState.default(addresses[index]);
Expand Down Expand Up @@ -182,20 +183,18 @@ export class LibraClient {
* numCoins should be in libraCoins based unit.
*
* @param sender
* @param receipientAddress
* @param recipientAddress
* @param numCoins
*/
public async transferCoins(
sender: Account,
receipientAddress: string,
recipientAddress: string,
numCoins: number | string | BigNumber,
): Promise<SubmitTransactionResponse> {
const response = await this.execute(
LibraTransaction.createTransfer(receipientAddress, new BigNumber(numCoins)),
sender,
return await this.execute(
LibraTransaction.createTransfer(recipientAddress, new BigNumber(numCoins)),
sender,
);

return response;
}

/**
Expand All @@ -207,7 +206,7 @@ export class LibraClient {
public async execute(transaction: LibraTransaction, sender: Account): Promise<SubmitTransactionResponse> {
let senderAddress = transaction.sendersAddress;
if (senderAddress.isDefault()) {
senderAddress = sender.getAddress();
senderAddress = sender.getAddress();
}
let sequenceNumber = transaction.sequenceNumber;
if (sequenceNumber.isNegative()) {
Expand All @@ -218,24 +217,36 @@ export class LibraClient {
// Still working on this part
const program = new Program();
program.setCode(transaction.program.code);
// program.setArgumentsList([new TransactionArgument()]);
// program.setModulesList([])

// TODO: Change grpc library. Some of this values should not be numbers
const transactionArguments = new Array<TransactionArgument>();
transaction.program.arguments.forEach(argument => {
const transactionArgument = new TransactionArgument();
transactionArgument.setType(argument.type);
transactionArgument.setData(argument.value);
transactionArguments.push(transactionArgument);
});
program.setArgumentsList(transactionArguments);
program.setModulesList(transaction.program.modules);
const rawTransaction = new RawTransaction();
rawTransaction.setSenderAccount(senderAddress.toBytes());
rawTransaction.setExpirationTime(transaction.expirationTime.toNumber());
rawTransaction.setGasUnitPrice(transaction.gasContraint.gasUnitPrice.toNumber());
rawTransaction.setMaxGasAmount(transaction.gasContraint.maxGasAmount.toNumber());
rawTransaction.setSequenceNumber(sequenceNumber.toNumber());
rawTransaction.setProgram(program);
rawTransaction.setMaxGasAmount(transaction.gasContraint.maxGasAmount.toNumber());
rawTransaction.setGasUnitPrice(transaction.gasContraint.gasUnitPrice.toNumber());
rawTransaction.setExpirationTime(transaction.expirationTime.toNumber());
rawTransaction.setSenderAccount(senderAddress.toBytes());

const signedTransaction = new SignedTransaction();
signedTransaction.setSenderPublicKey(sender.keyPair.getPublicKey());
signedTransaction.setSenderSignature('');
signedTransaction.setRawTxnBytes('');

const request = new SubmitTransactionRequest();
const rawTxnBytes = rawTransaction.serializeBinary();
const hash = new SHA3(256)
.update(Buffer.from(HashSaltValues.rawTransactionHashSalt, 'hex'))
.update(Buffer.from(rawTxnBytes.buffer))
.digest();
const senderSignature = sender.keyPair.sign(hash);
signedTransaction.setRawTxnBytes(rawTxnBytes);
signedTransaction.setSenderPublicKey(sender.keyPair.getPublicKey());
signedTransaction.setSenderSignature(senderSignature);

request.setSignedTxn(signedTransaction);
return new Promise((resolve, reject) => {
this.client.submitTransaction(request, (error: ServiceError | null, response: SubmitTransactionResponse) => {
Expand Down
4 changes: 4 additions & 0 deletions lib/constants/HashSaltValues.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default {
// in order to not recompute path values hashes, I decided to store the hashes as constants
rawTransactionHashSalt: '46f174df6ca8de5ad29745f91584bb913e7df8dd162e3e921a5c1d8637c88d16',
};
3 changes: 3 additions & 0 deletions lib/constants/ProgamBase64Codes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default {
peerToPeerTxn: 'TElCUkFWTQoBAAcBSgAAAAQAAAADTgAAAAYAAAAMVAAAAAUAAAANWQAAAAQAAAAFXQAAACkAAAAEhgAAACAAAAAHpgAAAA0AAAAAAAABAAIAAQMAAgACBAIDAgQCBjxTRUxGPgxMaWJyYUFjY291bnQEbWFpbg9wYXlfZnJvbV9zZW5kZXIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAgAEAAwADAERAQI=',
};
3 changes: 2 additions & 1 deletion lib/crypto/Hkdf.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* tslint:disable */
const hkdf = require('futoin-hkdf');
/* tslint:enable */
type BuffString = Buffer | string;

// Todo: Update implementation to work not only with Node
export class Hkdf {
Expand All @@ -16,7 +17,7 @@ export class Hkdf {
return new Uint8Array(prk);
}

public expand(prk: Uint8Array, info: string, outputLen: number): Uint8Array {
public expand(prk: Uint8Array, info: BuffString, outputLen: number): Uint8Array {
const prkBuffer = Buffer.from(prk);
const okm = hkdf.expand(this.hashAlgorithm, this.hashLength, prkBuffer, outputLen, info);
return new Uint8Array(okm);
Expand Down
28 changes: 27 additions & 1 deletion lib/crypto/Pbkdf.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import crypto from 'crypto';
import crypto, {BinaryLike} from 'crypto';

export class Pbkdf {
private readonly digestAlgorithm: string;
Expand All @@ -9,4 +9,30 @@ export class Pbkdf {
public extract(password: Uint8Array, salt: string, iterations: number, outputLen: number) {
return new Uint8Array(crypto.pbkdf2Sync(Buffer.from(password), salt, iterations, outputLen, this.digestAlgorithm));
}

public pbkdf2(password: BinaryLike, salt: Buffer, iterations: number, outputLen: number) {
const hmacLength = crypto.createHmac(this.digestAlgorithm, 'test').digest().length;
const outputBuffer = Buffer.alloc(outputLen);
const hmacOutput = Buffer.alloc(hmacLength);
const block = Buffer.alloc(salt.length + 4);
const leftLength = Math.ceil(outputLen / hmacLength);
const rightLength = outputLen - (leftLength - 1) * hmacLength;
salt.copy(block, 0, 0, salt.length);
for (let i = 1; i <= leftLength; i++) {
block.writeUInt32BE(i, salt.length);
let hmac = crypto.createHmac(this.digestAlgorithm, password).update(block).digest();
hmac.copy(hmacOutput, 0, 0, hmacLength);
for (let j = 1; j < iterations; j++) {
hmac = crypto.createHmac(this.digestAlgorithm, password).update(hmac).digest();
for (let k = 0; k < hmacLength; k++) {
// tslint:disable-next-line:no-bitwise
hmacOutput[k] ^= hmac[k];
}
}
const destPos = (i - 1) * hmacLength;
const len = (i === leftLength ? rightLength : hmacLength);
hmacOutput.copy(outputBuffer, destPos, 0, len);
}
return outputBuffer;
}
}
13 changes: 7 additions & 6 deletions lib/wallet/KeyFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ import { Mnemonic } from './Mnemonic';
export class Seed {
public static fromMnemonic(words: string[] | Mnemonic, salt: string = 'LIBRA'): Seed {
const mnemonic: Mnemonic = Array.isArray(words) ? new Mnemonic(words) : words;
const mnemonicBytes = mnemonic.toBytes();
const parsedSalt = `${KeyPrefixes.MnemonicSalt}${salt}`;

const bytes = new Pbkdf('sha3-256').extract(mnemonicBytes, parsedSalt, 2048, 32);
const bytes = new Pbkdf('sha3-256').pbkdf2(mnemonic.toBytes(),
Buffer.from(`${KeyPrefixes.MnemonicSalt}${salt}`),
2048, 32);
return new Seed(bytes);
}
public readonly data: Uint8Array;
Expand Down Expand Up @@ -47,9 +46,11 @@ export class KeyFactory {
*
*/
public generateKey(childDepth: number): KeyPair {
const info = `${KeyPrefixes.DerivedKey}${childDepth}`;
const childDepthBuffer = Buffer.alloc(8);
childDepthBuffer.writeBigUInt64LE(BigInt(childDepth));
const info = Buffer.concat([Uint8Array.from(Buffer.from(KeyPrefixes.DerivedKey)),
Uint8Array.from(childDepthBuffer)]);
const secretKey = this.hkdf.expand(this.masterPrk, info, 32);

return KeyPair.fromSecretKey(secretKey);
}
}
2 changes: 1 addition & 1 deletion lib/wallet/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface WalletConfig {
export class LibraWallet {
private readonly config: WalletConfig;
private keyFactory: KeyFactory;
private lastChild = 1;
private lastChild = 0;
private accounts: { [address: string]: Account } = {};

constructor(config?: WalletConfig) {
Expand Down
Loading

0 comments on commit a5d1587

Please sign in to comment.