Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 139 additions & 34 deletions modules/core/src/v2/coins/algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
*/
import { BaseCoin } from '../baseCoin';
import * as _ from 'lodash';

const {
import {
NaclWrapper,
Multisig,
Address,
Expand All @@ -13,7 +12,69 @@ const {
generateAccount,
isValidAddress,
isValidSeed,
} = require('algosdk');
Encoding,
} from 'algosdk';

export interface TransactionExplanation {
displayOrder: string[];
id: string;
outputs: Output[],
changeOutputs: Output[],
outputAmount: string;
changeAmount: number;
fee: TransactionFee;
memo: string;
}

export interface SignTransactionOptions {
txPrebuild: TransactionPrebuild;
prv: string;
wallet: {
addressVersion: string;
}
keychain: KeyPair;
backupKeychain: KeyPair;
bitgoKeychain: KeyPair;
}

export interface TransactionPrebuild {
txBase64: string;
txInfo: {
from: string;
to: string;
amount: string;
fee: number;
firstRound: number;
lastRound: number;
genesisID: string;
genesisHash: string;
note?: string;
}
}

export interface HalfSignedTransaction {
halfSigned: {
txBase64: string,
}
}

export interface Output {
address: string;
amount: string;
}

export interface TransactionFee {
fee: number;
feeRate?: number;
size?: number
}

export interface ExplainTransactionOptions {
txBase64: string;
wallet: {
addressVersion: string;
}
}

interface KeyPair {
pub: string;
Expand Down Expand Up @@ -89,7 +150,7 @@ export class Algo extends BaseCoin {
* @param {String} prv the prv to be checked
* @returns {Boolean} is it valid?
*/
isValidPrv(prv): boolean {
isValidPrv(prv: string): boolean {
return isValidSeed(prv);
}

Expand All @@ -99,7 +160,7 @@ export class Algo extends BaseCoin {
* @param {String} address the pub to be checked
* @returns {Boolean} is it valid?
*/
isValidAddress(address): boolean {
isValidAddress(address: string): boolean {
return isValidAddress(address);
}

Expand All @@ -109,7 +170,7 @@ export class Algo extends BaseCoin {
* @param key
* @param message
*/
signMessage(key, message): Buffer {
signMessage(key: KeyPair, message: string | Buffer): Buffer {
// key.prv actually holds the encoded seed, but we use the prv name to avoid breaking the keypair schema.
// See jsdoc comment in isValidPrv
let seed = key.prv;
Expand All @@ -133,30 +194,79 @@ export class Algo extends BaseCoin {
}

/**
* Specifies what key we will need for signing - Algorand needs the backup, bitgo pubs.
* Specifies what key we will need for signing` - Algorand needs the backup, bitgo pubs.
*/
keyIdsForSigning() {
keyIdsForSigning(): number[] {
return [ 0, 1, 2 ];
}

/**
* Explain/parse transaction
* @param params
* - txBase64: transaction encoded as base64 string
*/
explainTransaction(params: ExplainTransactionOptions): TransactionExplanation {
const { txBase64 } = params;

let tx;
try {
const txToHex = Buffer.from(txBase64, 'base64');
const decodedTx = Encoding.decode(txToHex);

// if we are a signed msig tx, the structure actually has the { msig, txn } as the root object
// if we are not signed, the decoded tx is the txn - refer to partialSignTxn and MultiSig constructor
// in algosdk for more information
const txnForDecoding = decodedTx.txn || decodedTx;

tx = Multisig.MultiSigTransaction.from_obj_for_encoding(txnForDecoding);
} catch (ex) {
throw new Error('txBase64 needs to be a valid tx encoded as base64 string');
}

const id = tx.txID();
const fee = { fee: tx.fee};

const outputs = [{
amount: tx.amount,
address: Address.encode(new Uint8Array(tx.to.publicKey)),
}];

const outputAmount = tx.amount;

// TODO(CT-480): add recieving address display here
const memo = tx.note;

return {
displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'memo'],
id,
outputs,
outputAmount,
changeAmount: 0,
fee,
changeOutputs: [],
memo
};
}

/**
* Assemble keychain and half-sign prebuilt transaction
*
* @param params
* @param params.txPrebuild {Object} prebuild object returned by platform
* @param params.prv {String} user prv
* @param params.wallet.addressVersion {String} this is the version of the Algorand multisig address generation format
*/
signTransaction(params) {
signTransaction(params: SignTransactionOptions): HalfSignedTransaction {
const prv = params.prv;
const txData = params.txPrebuild.txData;
const txBase64 = params.txPrebuild.txBase64;
const addressVersion = params.wallet.addressVersion;

if (_.isUndefined(txData)) {
if (_.isUndefined(txBase64)) {
throw new Error('missing txPrebuild parameter');
}

if (!_.isObject(txData)) {
throw new Error(`txPrebuild must be an object, got type ${typeof txData}`);
if (!_.isString(txBase64)) {
throw new Error(`txPrebuild must be an object, got type ${typeof txBase64}`);
}

if (_.isUndefined(prv)) {
Expand All @@ -175,24 +285,6 @@ export class Algo extends BaseCoin {
throw new Error('missing addressVersion parameter to sign transaction');
}

const refinedTxData = txData;
refinedTxData.amount = parseInt(txData.amount, 10);

// if note is truly null, we need to pass an array
if (!_.has(txData, 'note')) {
refinedTxData.note = new Uint8Array(0);
} else {
// if note was passed as null, assume its an empty string
if (txData.note === null) {
txData.note = '';
}

// note has a maximum length
if (txData.note.length > MAX_ALGORAND_NOTE_LENGTH) {
throw new Error('note size exceeded specification');
}
}

// we need to re-encode our public keys using algosdk's format
const encodedPublicKeys = [
Address.decode(params.keychain.pub).publicKey,
Expand All @@ -205,15 +297,28 @@ export class Algo extends BaseCoin {
const pair = generateAccountFromSeed(seed);
const sk = pair.sk;

// decode our tx
let transaction;
try {
const txToHex = Buffer.from(txBase64, 'base64');
const decodedTx = Encoding.decode(txToHex);
Comment thread
argjv marked this conversation as resolved.
Outdated
transaction = Multisig.MultiSigTransaction.from_obj_for_encoding(decodedTx);
} catch (e) {
throw new Error('transaction needs to be a valid tx encoded as base64 string');
}

// sign
const transaction = new Multisig.MultiSigTransaction(refinedTxData);
const halfSigned = transaction.partialSignTxn(
{ version: addressVersion, threshold: 2, pks: encodedPublicKeys },
sk
);

const txHex = Buffer.from(halfSigned).toString('base64');
const signedBase64 = Buffer.from(halfSigned).toString('base64');

return { txHex };
return {
halfSigned: {
txBase64: signedBase64,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we returning the same data twice with two different names? Can we clean this up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this, yeah we didn't need to do this

}
};
}
}
17 changes: 17 additions & 0 deletions modules/core/src/v2/keychains.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import common = require('../common');
import * as _ from 'lodash';
import * as Promise from 'bluebird';
const util = require('../util');
const co = Promise.coroutine;

const Keychains = function(bitgo, baseCoin) {
Expand Down Expand Up @@ -207,5 +208,21 @@ Keychains.prototype.createBackup = function(params, callback) {
}).call(this).asCallback(callback);
};

/**
* Gets keys for signing from a wallet
* @param reqId
* @param callback
* @returns {Promise[]}
*/
Keychains.prototype.getKeysForSigning = function(params, callback) {
return co(function *() {
const reqId = params.reqId || util.createRequestId();
const ids = params.wallet.baseCoin.keyIdsForSigning();
const keychainQueriesPromises = ids.map(
id => this.get({ id: params.wallet.keyIds()[id], reqId })
);
return Promise.all(keychainQueriesPromises);
}).call(this).asCallback(callback);
};

module.exports = Keychains;
9 changes: 3 additions & 6 deletions modules/core/src/v2/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NodeCallback } from './types';

const PendingApproval = require('./pendingApproval');
const util = require('../util');
const Keychains = require('./keychains');

const debug = debugLib('bitgo:v2:wallet');
const co = Bluebird.coroutine;
Expand Down Expand Up @@ -1237,12 +1238,8 @@ export class Wallet {
const txPrebuildQuery = params.prebuildTx ? Promise.resolve(params.prebuildTx) : this.prebuildTransaction(params);

// retrieve our keychains needed to run the prebuild - some coins use all pubs
const ids = this.baseCoin.keyIdsForSigning();
const keychainQueriesPromises = ids.map(
keyId => this.baseCoin.keychains().get({ id: this._wallet.keys[keyId], reqId: params.reqId })
);
const keychains = yield Promise.all(keychainQueriesPromises);

const keychains = yield this.baseCoin.keychains().getKeysForSigning({ wallet: this, reqId: params.reqId });

const txPrebuild = yield txPrebuildQuery;

try {
Expand Down
4 changes: 3 additions & 1 deletion modules/core/test/v2/fixtures/algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,15 @@ module.exports.prebuild = function() {
txData: {
from: 'AWSC7RL3RM72HSUW5QU4XTX3AOHY7QD3WLUZC2CAHWP6BTI5Q7IABVUXTA',
to: 'XMHLMNAVJIMAW2RHJXLXKKK4G3J3U6VONNO3BTAQYVDC3MHTGDP3J5OCRU',
amount: '1000',
amount: 1000,
fee: 1000,
firstRound: 1000,
lastRound: 1999,
genesisID: 'testnet-v38.0',
genesisHash: '4HkOQEL2o2bVh2P1wSpky3s6cwcEg/AAd5qlery942g=',
note: new Uint8Array()
},
buildTxBase64: 'iaNhbXTNA+ijZmVlzgADsVCiZnbNA+ijZ2VurXRlc3RuZXQtdjM4LjCiZ2jEIOB5DkBC9qNm1Ydj9cEqZMt7OnMHBIPwAHeapXq8veNoomx2zQfPo3JjdsQguw62NBVKGAtqJ03XdSlcNtO6eq5rXbDMEMVGLbDzMN+jc25kxCAFpC/Fe4s/o8qW7CnLzvsDj4/Ae7LpkWhAPZ/gzR2H0KR0eXBlo3BheQ==',
userKeychain: {
pub: 'UMYEHZ2NNBYX43CU37LMINSHR362FT4GFVWL6V5IHPRCJVPZ46H6CBYLYE',
prv: 'HEHPVOKEINTQMW4K536BZWUZWQNKBXKIAUSEWCVX3LCXDHI2LVPMV6T5L4'
Expand Down
4 changes: 2 additions & 2 deletions modules/core/test/v2/unit/coins/algo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,15 @@ describe('ALGO:', function() {
it('should sign a prebuild', co(function *() {
// sign transaction
halfSignedTransaction = yield wallet.signTransaction({
txPrebuild: { txData: fixtures.txData },
txPrebuild: { txBase64: fixtures.buildTxBase64 },
prv: fixtures.userKeychain.prv,
keychain: fixtures.userKeychain,
backupKeychain: fixtures.backupKeychain,
bitgoKeychain: fixtures.bitgoKeychain,
wallet: { addressVersion: fixtures.walletData.coinSpecific.addressVersion }
});

halfSignedTransaction.txHex.should.equal(fixtures.signedTxBase64);
halfSignedTransaction.halfSigned.txBase64.should.equal(fixtures.signedTxBase64);
}));
});
});