Skip to content

Instantly share code, notes, and snippets.

@nikhiljha
Last active July 28, 2016 16:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nikhiljha/e5848853604bf54ec085dc8fe58b19f8 to your computer and use it in GitHub Desktop.
Save nikhiljha/e5848853604bf54ec085dc8fe58b19f8 to your computer and use it in GitHub Desktop.
NEM Signing/API Examples
'use strict';
define([
'definitions',
'jquery',
'utils/CryptoHelpers',
'utils/KeyPair',
'utils/TransactionType',
'utils/convert',
'services/SessionData'
], function(angular, $, CryptoHelpers, KeyPair, TransactionType, convert){
var mod = angular.module('walletApp.services');
mod.factory('Transactions', ['$http', 'sessionData',
function TransactionsFactory($http, sessionData) {
var o = {
};
var NEM_EPOCH = Date.UTC(2015, 2, 29, 0, 6, 25, 0);
var CURRENT_NETWORK_ID = sessionData.getNetworkId();
var _dummy = document.createElement('a');
_dummy.href = sessionData.getNode().uri;
var CURRENT_HOSTNAME = _dummy.hostname;
var CURRENT_NETWORK_VERSION = function(val) {
if (CURRENT_NETWORK_ID === 104) {
return 0x68000000 | val;
} else if (CURRENT_NETWORK_ID === -104) {
return 0x98000000 | val;
}
return 0x60000000 | val;
};
function CREATE_DATA(txtype, senderPublicKey, timeStamp, due, version)
{
return {
'type': txtype,
'version': version || CURRENT_NETWORK_VERSION(1),
'signer': senderPublicKey,
'timeStamp': timeStamp,
'deadline': timeStamp + due * 60
};
}
function CALC_MIN_FEE(numNem) {
return Math.ceil(Math.max(10 - numNem, 2, Math.floor(Math.atan(numNem / 150000.0) * 3 * 33)));
}
o.getTimeStamp = function() {
return Math.floor((Date.now() / 1000) - (NEM_EPOCH / 1000));
};
o._constructTransfer = function(senderPublicKey, recipientCompressedKey, amount, message, due, mosaics, mosaicsFee) {
var timeStamp = o.getTimeStamp();
var version = mosaics ? CURRENT_NETWORK_VERSION(2) : CURRENT_NETWORK_VERSION(1);
var data = CREATE_DATA(0x101, senderPublicKey, timeStamp, due, version);
var msgFee = message.payload.length ? Math.max(1, Math.floor(message.payload.length / 2 / 16)) * 2 : 0;
var fee = mosaics ? mosaicsFee : CALC_MIN_FEE(amount / 1000000);
var totalFee = (msgFee + fee) * 1000000;
var custom = {
'recipient': recipientCompressedKey.toUpperCase().replace(/-/g, ''),
'amount': amount,
'fee': totalFee,
'message': message,
'mosaics': mosaics
};
var entity = $.extend(data, custom);
//console.log(entity);
return entity;
};
//actualSender, namespaceParent, namespaceName
o._constructNamespace = function(senderPublicKey, rentalFeeSink, rentalFee, namespaceParent, namespaceName, due) {
var timeStamp = o.getTimeStamp();
var version = CURRENT_NETWORK_VERSION(1);
var data = CREATE_DATA(0x2001, senderPublicKey, timeStamp, due, version);
var fee = 2 * 3 * 18;
var totalFee = (fee) * 1000000;
var custom = {
'rentalFeeSink': rentalFeeSink.toUpperCase().replace(/-/g, ''),
'rentalFee': rentalFee,
'parent': namespaceParent,
'newPart': namespaceName,
'fee': totalFee
};
var entity = $.extend(data, custom);
//console.log(entity);
return entity;
};
o._constructMosaicDefinition = function(senderPublicKey, rentalFeeSink, rentalFee, namespaceParent, mosaicName, mosaicDescription, mosaicProperties, levy, due) {
var timeStamp = o.getTimeStamp();
var version = CURRENT_NETWORK_VERSION(1);
var data = CREATE_DATA(0x4001, senderPublicKey, timeStamp, due, version);
var fee = 2 * 3 * 18;
var totalFee = (fee) * 1000000;
var levyData = levy ? {
'type': levy.feeType,
'recipient': levy.address.toUpperCase().replace(/-/g, ''),
'mosaicId': levy.mosaic,
'fee': levy.fee,
} : null;
var custom = {
'creationFeeSink': rentalFeeSink.replace(/-/g, ''),
'creationFee': rentalFee,
'mosaicDefinition':{
'creator': senderPublicKey,
'id': {
'namespaceId': namespaceParent,
'name': mosaicName,
},
'description': mosaicDescription,
'properties': $.map(mosaicProperties, function(v,k){
return {'name':k, 'value':v.toString()};
}),
'levy': levyData
},
'fee': totalFee
};
var entity = $.extend(data, custom);
//console.log(entity);
return entity;
};
o._constructMosaicSupply = function(senderPublicKey, mosaicId, supplyType, delta, due) {
var timeStamp = o.getTimeStamp();
var version = CURRENT_NETWORK_VERSION(1);
var data = CREATE_DATA(0x4002, senderPublicKey, timeStamp, due, version);
var fee = 2 * 3 * 18;
var totalFee = (fee) * 1000000;
var custom = {
'mosaicId': mosaicId,
'supplyType': supplyType,
'delta': delta,
'fee': totalFee
};
var entity = $.extend(data, custom);
//console.log(entity);
return entity;
};
o._constructSignature = function(senderPublicKey, otherAccount, otherHash, due) {
var timeStamp = o.getTimeStamp();
var version = CURRENT_NETWORK_VERSION(1);
var data = CREATE_DATA(0x1002, senderPublicKey, timeStamp, due, version);
var totalFee = (2 * 3) * 1000000;
var custom = {
'otherHash': { 'data': otherHash },
'otherAccount': otherAccount,
'fee': totalFee,
};
var entity = $.extend(data, custom);
return entity;
};
o._multisigWrapper = function(senderPublicKey, innerEntity, due) {
var timeStamp = o.getTimeStamp();
var version = CURRENT_NETWORK_VERSION(1);
var data = CREATE_DATA(0x1004, senderPublicKey, timeStamp, due, version);
var custom = {
'fee': 18000000,
'otherTrans': innerEntity
};
var entity = $.extend(data, custom);
//console.log("_multisigWrapper: ", entity);
return entity;
};
/**
* NOTE, related to serialization: Unfortunately we need to create few objects
* and do a bit of copying, as Uint32Array does not allow random offsets
*/
/* safe string - each char is 8 bit */
o._serializeSafeString = function(str) {
var r = new ArrayBuffer(132);
var d = new Uint32Array(r);
var b = new Uint8Array(r);
var e = 4;
if (str === null) {
d[0] = 0xffffffff;
} else {
d[0] = str.length;
for (var j = 0; j < str.length; ++j) {
b[e++] = str.charCodeAt(j);
}
}
return new Uint8Array(r, 0, e);
};
o._serializeUaString = function(str) {
var r = new ArrayBuffer(516);
var d = new Uint32Array(r);
var b = new Uint8Array(r);
var e = 4;
if (str === null) {
d[0] = 0xffffffff;
} else {
d[0] = str.length;
for (var j = 0; j < str.length; ++j) {
b[e++] = str[j];
}
}
return new Uint8Array(r, 0, e);
};
o._serializeLong = function(value) {
var r = new ArrayBuffer(8);
var d = new Uint32Array(r);
d[0] = value;
d[1] = Math.floor((value / 0x100000000));
return new Uint8Array(r, 0, 8);
};
o._serializeMosaicId = function(mosaicId) {
var r = new ArrayBuffer(264);
var serializedNamespaceId = o._serializeSafeString(mosaicId.namespaceId);
var serializedName = o._serializeSafeString(mosaicId.name);
var b = new Uint8Array(r);
var d = new Uint32Array(r);
d[0] = serializedNamespaceId.length + serializedName.length;
var e = 4;
for (var j=0; j<serializedNamespaceId.length; ++j) {
b[e++] = serializedNamespaceId[j];
}
for (var j=0; j<serializedName.length; ++j) {
b[e++] = serializedName[j];
}
return new Uint8Array(r, 0, e);
};
o._serializeMosaicAndQuantity = function(mosaic) {
var r = new ArrayBuffer(4 + 264 + 8);
var serializedMosaicId = o._serializeMosaicId(mosaic.mosaicId);
var serializedQuantity = o._serializeLong(mosaic.quantity);
//console.log(convert.ua2hex(serializedQuantity), serializedMosaicId, serializedQuantity);
var b = new Uint8Array(r);
var d = new Uint32Array(r);
d[0] = serializedMosaicId.length + serializedQuantity.length;
var e = 4;
for (var j=0; j<serializedMosaicId.length; ++j) {
b[e++] = serializedMosaicId[j];
}
for (var j=0; j<serializedQuantity.length; ++j) {
b[e++] = serializedQuantity[j];
}
return new Uint8Array(r, 0, e);
};
o._serializeMosaics = function(entity) {
var r = new ArrayBuffer(276*10 + 4);
var d = new Uint32Array(r);
var b = new Uint8Array(r);
var i = 0;
var e = 0;
d[i++] = entity.length;
e += 4;
var temporary = [];
for (var j=0; j<entity.length; ++j) {
temporary.push({'entity':entity[j], 'value':mosaicIdToName(entity[j].mosaicId) + " : " + entity[j].quantity})
}
temporary.sort(function(a, b) {return a.value < b.value ? -1 : a.value > b.value;});
for (var j=0; j<temporary.length; ++j) {
var entity = temporary[j].entity;
var serializedMosaic = o._serializeMosaicAndQuantity(entity);
for (var k=0; k<serializedMosaic.length; ++k) {
b[e++] = serializedMosaic[k];
}
}
return new Uint8Array(r, 0, e);
};
o._serializeProperty = function(entity) {
var r = new ArrayBuffer(1024);
var d = new Uint32Array(r);
var b = new Uint8Array(r);
var serializedName = o._serializeSafeString(entity['name']);
var serializedValue = o._serializeSafeString(entity['value']);
d[0] = serializedName.length + serializedValue.length;
var e = 4;
for (var j = 0; j<serializedName.length; ++j) { b[e++] = serializedName[j]; }
for (var j = 0; j<serializedValue.length; ++j) { b[e++] = serializedValue[j]; }
return new Uint8Array(r, 0, e);
};
o._serializeProperties = function(entity) {
var r = new ArrayBuffer(1024);
var d = new Uint32Array(r);
var b = new Uint8Array(r);
var i = 0;
var e = 0;
d[i++] = entity.length;
e += 4;
var temporary = entity;
var temporary = [];
for (var j=0; j<entity.length; ++j) {
temporary.push(entity[j]);
}
var helper = {'divisibility':1, 'initialSupply':2, 'supplyMutable':3, 'transferable':4};
temporary.sort(function(a, b) {return helper[a.name] < helper[b.name] ? -1 : helper[a.name] > helper[b.name];});
for (var j=0; j<temporary.length; ++j) {
var entity = temporary[j];
var serializedProperty = o._serializeProperty(entity);
for (var k=0; k<serializedProperty.length; ++k) {
b[e++] = serializedProperty[k];
}
}
return new Uint8Array(r, 0, e);
};
o._serializeLevy = function(entity) {
var r = new ArrayBuffer(1024);
var d = new Uint32Array(r);
if (entity === null)
{
d[0] = 0;
return new Uint8Array(r, 0, 4);
}
var b = new Uint8Array(r);
d[1] = entity['type'];
var e = 8;
var temp = o._serializeSafeString(entity['recipient']);
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
var serializedMosaicId = o._serializeMosaicId(entity['mosaicId']);
for (var j=0; j<serializedMosaicId.length; ++j) {
b[e++] = serializedMosaicId[j];
}
var serializedFee = o._serializeLong(entity['fee']);
for (var j=0; j<serializedFee.length; ++j) {
b[e++] = serializedFee[j];
}
d[0] = 4 + temp.length + serializedMosaicId.length + 8;
return new Uint8Array(r, 0, e);
};
o._serializeMosaicDefinition = function(entity) {
var r = new ArrayBuffer(40 + 264 + 516 + 1024 + 1024);
var d = new Uint32Array(r);
var b = new Uint8Array(r);
var temp = convert.hex2ua(entity['creator']);
d[0] = temp.length;
var e = 4;
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
var serializedMosaicId = o._serializeMosaicId(entity.id);
for (var j=0; j<serializedMosaicId.length; ++j) {
b[e++] = serializedMosaicId[j];
}
var utf8ToUa = convert.hex2ua(convert.utf8ToHex(entity['description']));
var temp = o._serializeUaString(utf8ToUa);
for (var j=0; j<temp.length; ++j) {
b[e++] = temp[j];
}
var temp = o._serializeProperties(entity['properties']);
for (var j=0; j<temp.length; ++j) {
b[e++] = temp[j];
}
var levy = o._serializeLevy(entity['levy']);
for (var j=0; j<levy.length; ++j) {
b[e++] = levy[j];
}
return new Uint8Array(r, 0, e);
};
o.serializeTransaction = function(entity) {
var r = new ArrayBuffer(512 + 2764);
var d = new Uint32Array(r);
var b = new Uint8Array(r);
d[0] = entity['type'];
d[1] = entity['version'];
d[2] = entity['timeStamp'];
var temp = convert.hex2ua(entity['signer']);
d[3] = temp.length;
var e = 16;
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
// Transaction
var i = e / 4;
d[i++] = entity['fee'];
d[i++] = Math.floor((entity['fee'] / 0x100000000));
d[i++] = entity['deadline'];
e += 12;
// TransferTransaction
if (d[0] === TransactionType.Transfer) {
d[i++] = entity['recipient'].length;
e += 4;
// TODO: check that entity['recipient'].length is always 40 bytes
for (var j = 0; j < entity['recipient'].length; ++j) {
b[e++] = entity['recipient'].charCodeAt(j);
}
i = e / 4;
d[i++] = entity['amount'];
d[i++] = Math.floor((entity['amount'] / 0x100000000));
e += 8;
if (entity['message']['type'] === 1 || entity['message']['type'] === 2) {
var temp = convert.hex2ua(entity['message']['payload']);
if (temp.length === 0) {
d[i++] = 0;
e += 4;
} else {
// length of a message object
d[i++] = 8 + temp.length;
// object itself
d[i++] = entity['message']['type'];
d[i++] = temp.length;
e += 12;
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
}
}
var entityVersion = d[1] & 0xffffff;
if (entityVersion >= 2) {
var temp = o._serializeMosaics(entity['mosaics']);
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
}
// Provision Namespace transaction
} else if (d[0] === TransactionType.ProvisionNamespace) {
d[i++] = entity['rentalFeeSink'].length;
e += 4;
// TODO: check that entity['rentalFeeSink'].length is always 40 bytes
for (var j = 0; j < entity['rentalFeeSink'].length; ++j) {
b[e++] = entity['rentalFeeSink'].charCodeAt(j);
}
i = e / 4;
d[i++] = entity['rentalFee'];
d[i++] = Math.floor((entity['rentalFee'] / 0x100000000));
e += 8;
var temp = o._serializeSafeString(entity['newPart']);
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
var temp = o._serializeSafeString(entity['parent']);
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
// Mosaic Definition Creation transaction
} else if (d[0] === TransactionType.MosaicDefinition) {
var temp = o._serializeMosaicDefinition(entity['mosaicDefinition']);
d[i++] = temp.length;
e += 4;
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
temp = o._serializeSafeString(entity['creationFeeSink']);
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
temp = o._serializeLong(entity['creationFee']);
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
// Mosaic Supply Change transaction
} else if (d[0] === TransactionType.MosaicSupply) {
var serializedMosaicId = o._serializeMosaicId(entity['mosaicId']);
for (var j=0; j<serializedMosaicId.length; ++j) {
b[e++] = serializedMosaicId[j];
}
var temp = new ArrayBuffer(4);
d = new Uint32Array(temp);
d[0] = entity['supplyType'];
var serializeSupplyType = new Uint8Array(temp);
for (var j=0; j<serializeSupplyType.length; ++j) {
b[e++] = serializeSupplyType[j];
}
var serializedDelta = o._serializeLong(entity['delta']);
for (var j=0; j<serializedDelta.length; ++j) {
b[e++] = serializedDelta[j];
}
// Signature transaction
} else if (d[0] === TransactionType.MultisigSignature) {
var temp = convert.hex2ua(entity['otherHash']['data']);
// length of a hash object....
d[i++] = 4 + temp.length;
// object itself
d[i++] = temp.length;
e += 8;
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
i = e / 4;
temp = entity['otherAccount'];
d[i++] = temp.length;
e += 4;
for (var j = 0; j < temp.length; ++j) {
b[e++] = temp.charCodeAt(j);
}
// Multisig wrapped transaction
} else if (d[0] === TransactionType.MultisigTransaction) {
var temp = o.serializeTransaction(entity['otherTrans']);
d[i++] = temp.length;
e += 4;
for (var j = 0; j<temp.length; ++j) { b[e++] = temp[j]; }
}
return new Uint8Array(r, 0, e);
};
o.prepareMessage = function prepareMessage(common, tx) {
if (tx.encryptMessage) {
if (!tx.recipientPubKey || !tx.message || !common.privatekey) {
return {'type':0, 'payload':''};
}
return {'type':2, 'payload':CryptoHelpers.encode(common.privatekey, tx.recipientPubKey, tx.message.toString())};
}
return {'type': 1, 'payload':convert.utf8ToHex(tx.message.toString())}
};
o.prepareTransfer = function(common, tx) {
//console.log('prepareTransfer', tx);
var kp = KeyPair.create(common.privatekey);
var actualSender = tx.isMultisig ? tx.multisigAccount.publicKey : kp.publicKey.toString();
var recipientCompressedKey = tx.recipient.toString();
var amount = parseInt(tx.amount * 1000000, 10);
var message = o.prepareMessage(common, tx);
var due = tx.due;
var mosaics = null;
var mosaicsFee = null;
var entity = o._constructTransfer(actualSender, recipientCompressedKey, amount, message, due, mosaics, mosaicsFee);
if (tx.isMultisig) {
entity = o._multisigWrapper(kp.publicKey.toString(), entity, due);
}
return entity;
};
function mosaicIdToName(mosaicId) {
return mosaicId.namespaceId + ":" + mosaicId.name;
}
function calcXemEquivalent(multiplier, q, sup, divisibility) {
if (sup === 0) {
return 0;
}
// TODO: can this go out of JS (2^54) bounds? (possible BUG)
return 8999999999 * q * multiplier / sup / Math.pow(10, divisibility + 6);
}
o.calculateMosaicsFee = function(multiplier, mosaics, attachedMosaics) {
var totalFee = 0;
for (var m of attachedMosaics) {
// TODO: copied from filters, refactor
var mosaicName = mosaicIdToName(m.mosaicId);
if (!(mosaicName in mosaics)) { return ['unknown mosaic divisibility', data]; }
var mosaicDefinitionMetaDataPair = mosaics[mosaicName];
var divisibilityProperties = $.grep(mosaicDefinitionMetaDataPair.mosaicDefinition.properties, function(w){ return w.name === "divisibility"; });
var divisibility = divisibilityProperties.length === 1 ? ~~(divisibilityProperties[0].value) : 0;
//var supply = mosaicDefinitionMetaDataPair.meta.supply;
var supply = mosaicDefinitionMetaDataPair.supply;
var quantity = m.quantity;
var numNem = calcXemEquivalent(multiplier, quantity, supply, divisibility);
var fee = CALC_MIN_FEE(numNem);
//console.log("CALCULATING FEE for ", m, mosaicDefinitionMetaDataPair, "divisibility", divisibility, "nem equivalent", numNem, "calculated fee", fee);
totalFee += fee;
}
return (totalFee * 5) / 4;
};
o.prepareTransferV2 = function(common, mosaicsMetaData, tx) {
//console.log('prepareTransferV2', tx);
var kp = KeyPair.create(common.privatekey);
var actualSender = tx.isMultisig ? tx.multisigAccount.publicKey : kp.publicKey.toString();
var recipientCompressedKey = tx.recipient.toString();
// multiplier
var amount = parseInt(tx.multiplier * 1000000, 10);
var message = o.prepareMessage(common, tx);
var due = tx.due;
var mosaics = tx.mosaics;
var mosaicsFee = o.calculateMosaicsFee(amount, mosaicsMetaData, mosaics);
var entity = o._constructTransfer(actualSender, recipientCompressedKey, amount, message, due, mosaics, mosaicsFee);
if (tx.isMultisig) {
entity = o._multisigWrapper(kp.publicKey.toString(), entity, due);
}
return entity;
};
o.prepareNamespace = function(common, tx) {
var kp = KeyPair.create(common.privatekey);
var actualSender = tx.isMultisig ? tx.multisigAccount.publicKey : kp.publicKey.toString();
var rentalFeeSink = tx.rentalFeeSink.toString();
var rentalFee = tx.rentalFee;
var namespaceParent = tx.namespaceParent ? tx.namespaceParent.fqn : null;
var namespaceName = tx.namespaceName.toString();
var due = tx.due;
var entity = o._constructNamespace(actualSender, rentalFeeSink, rentalFee, namespaceParent, namespaceName, due);
if (tx.isMultisig) {
entity = o._multisigWrapper(kp.publicKey.toString(), entity, due);
}
return entity;
};
o.prepareMosaicDefinition = function(common, tx) {
var kp = KeyPair.create(common.privatekey);
var actualSender = tx.isMultisig ? tx.multisigAccount.publicKey : kp.publicKey.toString();
var rentalFeeSink = tx.mosaicFeeSink.toString();
var rentalFee = tx.mosaicFee;
var namespaceParent = tx.namespaceParent.fqn;
var mosaicName = tx.mosaicName.toString();
var mosaicDescription = tx.mosaicDescription.toString();
var mosaicProperties = tx.properties;
var levy = tx.levy.mosaic ? tx.levy : null;
var due = tx.due;
var entity = o._constructMosaicDefinition(actualSender, rentalFeeSink, rentalFee, namespaceParent, mosaicName, mosaicDescription, mosaicProperties, levy, due);
if (tx.isMultisig) {
entity = o._multisigWrapper(kp.publicKey.toString(), entity, due);
}
return entity;
};
o.prepareMosaicSupply = function(common, tx) {
var kp = KeyPair.create(common.privatekey);
var actualSender = tx.isMultisig ? tx.multisigAccount.publicKey : kp.publicKey.toString();
var due = tx.due;
var entity = o._constructMosaicSupply(actualSender, tx.mosaic, tx.supplyType, tx.delta, due);
if (tx.isMultisig) {
entity = o._multisigWrapper(kp.publicKey.toString(), entity, due);
}
return entity;
};
o.prepareSignature = function(common, tx, nisPort, cb, failedCb) {
var kp = KeyPair.create(fixPrivateKey(common.privatekey));
var actualSender = kp.publicKey.toString();
var otherAccount = tx.multisigAccountAddress.toString();
var otherHash = tx.hash.toString();
var due = tx.due;
var entity = o._constructSignature(actualSender, otherAccount, otherHash, due);
var result = o.serializeTransaction(entity);
var signature = kp.sign(result);
var obj = {'data':convert.ua2hex(result), 'signature':signature.toString()};
/*
$http.post('http://'+CURRENT_HOSTNAME+':7890/transaction/prepare', entity).then(function (data){
var serializedTx = data.data;
var signature = kp.sign(serializedTx.data);
var obj = {'data':serializedTx.data, 'signature':signature.toString()};
console.log('nis', obj.data);
console.log(' js', convert.ua2hex(result));
}, function(data) {
failedCb('prepare', data);
});
/*/
return $http.post('http://'+CURRENT_HOSTNAME+':'+nisPort+'/transaction/announce', obj).then(function (data){
cb(data);
}, function(data) {
failedCb('announce', data);
});
//*/
};
function fixPrivateKey(privatekey) {
return ("0000000000000000000000000000000000000000000000000000000000000000" + privatekey.replace(/^00/, '')).slice(-64);
}
o.serializeAndAnnounceTransaction = function(entity, common, tx, nisPort, cb, failedCb) {
var kp = KeyPair.create(fixPrivateKey(common.privatekey));
var result = o.serializeTransaction(entity);
var signature = kp.sign(result);
var obj = {'data':convert.ua2hex(result), 'signature':signature.toString()};
/* leaving this here for testing purposes *
$http.post('http://'+CURRENT_HOSTNAME+':7890/transaction/prepare', entity).then(function (data){
var serializedTx = data.data;
var signature = kp.sign(serializedTx.data);
var obj = {'data':serializedTx.data, 'signature':signature.toString()};
console.log('nis', obj.data);
console.log(' js', convert.ua2hex(result));
}, function(data) {
failedCb('prepare', data);
});
/*/
return $http.post('http://'+CURRENT_HOSTNAME+':'+nisPort+'/transaction/announce', obj).then(function (data){
cb(data);
}, function(data) {
failedCb('announce', data);
});
//*/
};
return o;
}]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/Address',
'utils/CryptoHelpers',
'filters/filters',
'services/Transactions'
], function(angular, $, Address, CryptoHelpers) {
var mod = angular.module('walletApp.controllers');
mod.controller('TxCosignatureCtrl',
["$scope", "$window", "$q", "$timeout", "Transactions", 'walletScope', 'parent', 'meta',
function($scope, $window, $q, $timeout, Transactions, walletScope, parent, meta) {
$scope.walletScope = walletScope;
$scope.storage = $window.localStorage;
$scope.storage.setDefault('txCosignDefaults', {});
// load data from storage
$scope.common = {
'requiresKey': $scope.walletScope.sessionData.getRememberedKey() === undefined,
'password': '',
'privatekey': '',
};
$scope.txCosignData = {
'fee': $scope.storage.getObject('txCosignDefaults').fee || 0,
'due': $scope.storage.getObject('txCosignDefaults').due || (24 * 60),
'multisigAccount': parent.otherTrans.signer, // inner tx signer is a multisig account
'multisigAccountAddress': Address.toAddress(parent.otherTrans.signer, $scope.walletScope.networkId),
'hash': meta.innerHash.data, // hash of an inner tx is needed
};
// fix old default
var ver = $scope.storage.getObject('txCosignDefaults').ver;
if (! ver) {
$scope.txCosignData.due = 24 * 60;
}
$scope.$watchGroup(['common.password', 'common.privatekey'], function(nv,ov){
$scope.invalidKeyOrPassword = false;
});
$scope.okPressed = false;
$scope.ok = function txCosignOk() {
$scope.okPressed = true;
$timeout(function txCosignDeferred(){
$scope._ok().then(function(){
$scope.okPressed = false;
}, function(){
$scope.okPressed = false;
});
});
};
$scope._ok = function txCosign_Ok() {
// save most recent data
var orig = $scope.storage.getObject('txCosignDefaults')
$.extend(orig, {
'fee':$scope.txCosignData.fee,
'due':$scope.txCosignData.due,
'ver': 1
});
$scope.storage.setObject('txCosignDefaults', orig);
var rememberedKey = $scope.walletScope.sessionData.getRememberedKey();
if (rememberedKey) {
$scope.common.privatekey = CryptoHelpers.decrypt(rememberedKey);
} else {
if (! CryptoHelpers.passwordToPrivatekey($scope.common, $scope.walletScope.networkId, $scope.walletScope.walletAccount) ) {
$scope.invalidKeyOrPassword = true;
return $q.resolve(0);
}
}
return Transactions.prepareSignature($scope.common, $scope.txCosignData, $scope.walletScope.nisPort,
function(data) {
if (data.status === 200) {
if (data.data.code >= 2) {
alert('failed when trying to send tx: ' + data.data.message);
} else {
$scope.$close();
}
}
if (rememberedKey) { delete $scope.common.privatekey; }
},
function(operation, data) {
// will do for now, will change it to modal later
alert('failed at '+operation + " " + data.data.error + " " + data.data.message);
if (rememberedKey) { delete $scope.common.privatekey; }
}
);
}; // $scope._ok
$scope.cancel = function () {
$scope.$dismiss();
};
}
]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/CryptoHelpers',
'filters/filters',
], function(angular, $, CryptoHelpers) {
var mod = angular.module('walletApp.controllers');
mod.controller('TxDetailsCtrl',
["$scope", 'walletScope', 'parent', 'tx', 'meta',
function($scope, walletScope, parent, tx, meta) {
$scope.walletScope = walletScope;
$scope.parent = parent;
$scope.tx = tx;
$scope.meta = meta;
$scope.ok = function () {
$scope.$close();
};
}
]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/CryptoHelpers',
'sinks',
'filters/filters',
'services/Transactions'
], function(angular, $, CryptoHelpers, sinks) {
var mod = angular.module('walletApp.controllers');
mod.controller('TxMosaicCtrl',
["$scope", "$window", "$q", "$timeout", "Transactions", 'walletScope',
function($scope, $window, $q, $timeout, Transactions, walletScope) {
$scope.walletScope = walletScope;
$scope.storage = $window.localStorage;
$scope.storage.setDefault('txMosaicDefaults', {});
// begin tracking currently selected account and it's mosaics
$scope._updateCurrentAccount = function() {
var acct = $scope.walletScope.accountData.account.address
if ($scope.txMosaicData.isMultisig) {
acct = $scope.txMosaicData.multisigAccount.address;
}
$scope.currentAccount = acct;
};
$scope.selectTab = function selectTab(v) {
if (v === 'multisig') {
$scope.txMosaicData.isMultisig = true;
} else {
$scope.txMosaicData.isMultisig = false;
}
$scope.updateCurrentAccountMosaics();
};
$scope.updateCurrentAccountMosaics = function updateCurrentAccountMosaics() {
$scope._updateCurrentAccount();
var acct = $scope.currentAccount;
// we could do it without separate variable, but we want keys to be sorted
$scope.currentAccountMosaicNames = Object.keys($scope.walletScope.mosaicOwned[acct]).sort();
$scope.selectedMosaic = "nem:xem";
var ownedNamespaces = walletScope.namespaceOwned[acct];
if (ownedNamespaces) {
$scope.txMosaicData.namespaceParent = ownedNamespaces[Object.keys(ownedNamespaces)[0]];
} else {
alert("this account does not own any namespaces, try choosing non-multisig or a different account");
}
};
// end begin tracking currently selected account and it's mosaics
// load data from storage
$scope.common = {
'requiresKey': $scope.walletScope.sessionData.getRememberedKey() === undefined,
'password': '',
'privatekey': '',
};
$scope.txMosaicData = {
'mosaicFeeSink': '',
'mosaicFee': 50000 * 1000000,
'mosaicName': '',
'namespaceParent': '',
'mosaicDescription': $scope.storage.getObject('txMosaicDefaults').mosaicDescription || '',
'properties': {'initialSupply':0, 'divisibility':0, 'transferable':true, 'supplyMutable':true},
'levy':{'mosaic':null, 'address':'', 'feeType':1, 'fee':5},
'fee': 0,
'innerFee': 0,
'due': $scope.storage.getObject('txMosaicDefaults').due || 60,
'isMultisig': ($scope.storage.getObject('txMosaicDefaults').isMultisig && walletScope.accountData.meta.cosignatoryOf.length > 0) || false,
'multisigAccount': walletScope.accountData.meta.cosignatoryOf.length == 0?'':walletScope.accountData.meta.cosignatoryOf[0]
};
$scope.txMosaicData.mosaicFeeSink = sinks.mosaic[$scope.walletScope.networkId];
$scope.hasLevy = false;
function updateFee() {
var entity = Transactions.prepareMosaicDefinition($scope.common, $scope.txMosaicData);
$scope.txMosaicData.fee = entity.fee;
if ($scope.txMosaicData.isMultisig) {
$scope.txMosaicData.innerFee = entity.otherTrans.fee;
}
}
$scope.$watchGroup(['common.password', 'common.privatekey'], function(nv,ov){
$scope.invalidKeyOrPassword = false;
});
$scope.$watchGroup(['txMosaicData.isMultisig'], function(nv, ov){
updateFee();
});
$scope.$watch('selectedMosaic', function(){
if ($scope.hasLevy) {
$scope.txMosaicData.levy.mosaic = $scope.walletScope.mosaicOwned[$scope.currentAccount][$scope.selectedMosaic].mosaicId;
} else {
$scope.txMosaicData.levy.mosaic = null;
}
});
$scope.updateCurrentAccountMosaics();
$scope.okPressed = false;
$scope.ok = function txMosaicOk() {
$scope.okPressed = true;
$timeout(function txMosaicDeferred(){
$scope._ok().then(function(){
$scope.okPressed = false;
}, function(){
$scope.okPressed = false;
});
});
};
$scope._ok = function txMosaic_Ok() {
var orig = $scope.storage.getObject('txMosaicDefaults');
$.extend(orig, {
'due': $scope.txMosaicData.due,
'isMultisig': $scope.txMosaicData.isMultisig,
});
$scope.storage.setObject('txMosaicDefaults', orig);
var rememberedKey = $scope.walletScope.sessionData.getRememberedKey();
if (rememberedKey) {
$scope.common.privatekey = CryptoHelpers.decrypt(rememberedKey);
} else {
if (! CryptoHelpers.passwordToPrivatekey($scope.common, $scope.walletScope.networkId, $scope.walletScope.walletAccount) ) {
$scope.invalidKeyOrPassword = true;
return $q.resolve(0);
}
}
var entity = Transactions.prepareMosaicDefinition($scope.common, $scope.txMosaicData);
return Transactions.serializeAndAnnounceTransaction(entity, $scope.common, $scope.txMosaicData, $scope.walletScope.nisPort,
function(data) {
if (data.status === 200) {
if (data.data.code >= 2) {
alert('failed when trying to send tx: ' + data.data.message);
} else {
$scope.$close();
}
}
if (rememberedKey) { delete $scope.common.privatekey; }
},
function(operation, data) {
// will do for now, will change it to modal later
alert('failed at '+operation + " " + data.data.error + " " + data.data.message);
if (rememberedKey) { delete $scope.common.privatekey; }
}
);
}; // $scope._ok
$scope.cancel = function () {
$scope.$dismiss();
};
}
]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/CryptoHelpers',
'filters/filters',
'services/Transactions'
], function(angular, $, CryptoHelpers) {
var mod = angular.module('walletApp.controllers');
mod.controller('TxMosaicSupplyCtrl',
["$scope", "$window", "$timeout", "Transactions", 'walletScope',
function($scope, $window, $timeout, Transactions, walletScope) {
$scope.walletScope = walletScope;
$scope.storage = $window.localStorage;
$scope.storage.setDefault('txMosaicSupplyDefaults', {});
// begin tracking currently selected account and it's mosaics
$scope._updateCurrentAccount = function() {
var acct = $scope.walletScope.accountData.account.address
if ($scope.txMosaicSupplyData.isMultisig) {
acct = $scope.txMosaicSupplyData.multisigAccount.address;
}
$scope.currentAccount = acct;
};
$scope.selectTab = function selectTab(v) {
if (v === 'multisig') {
$scope.txMosaicSupplyData.isMultisig = true;
} else {
$scope.txMosaicSupplyData.isMultisig = false;
}
$scope.updateCurrentAccountMosaics();
};
$scope.updateCurrentAccountMosaics = function updateCurrentAccountMosaics() {
$scope._updateCurrentAccount();
var acct = $scope.currentAccount;
$scope.currentAccountMosaicNames = Object.keys($scope.walletScope.mosaicOwned[acct]).sort();
$scope.selectedMosaic = "nem:xem";
};
// end begin tracking currently selected account and it's mosaics
// load data from storage
$scope.common = {
'requiresKey': $scope.walletScope.sessionData.getRememberedKey() === undefined,
'password': '',
'privatekey': '',
};
$scope.txMosaicSupplyData = {
'mosaic': '',
'supplyType': 1,
'delta': 0,
'fee': 0,
'innerFee': 0,
'due': $scope.storage.getObject('txMosaicSupplyDefaults').due || 60,
'isMultisig': ($scope.storage.getObject('txMosaicSupplyDefaults').isMultisig && walletScope.accountData.meta.cosignatoryOf.length > 0) || false,
'multisigAccount': walletScope.accountData.meta.cosignatoryOf.length == 0?'':walletScope.accountData.meta.cosignatoryOf[0]
};
function updateFee() {
var entity = Transactions.prepareMosaicSupply($scope.common, $scope.txMosaicSupplyData);
$scope.txMosaicSupplyData.fee = entity.fee;
if ($scope.txMosaicSupplyData.isMultisig) {
$scope.txMosaicSupplyData.innerFee = entity.otherTrans.fee;
}
}
$scope.$watchGroup(['txMosaicSupplyData.isMultisig'], function(nv, ov){
updateFee();
});
$scope.$watchGroup(['common.password', 'common.privatekey'], function(nv,ov){
$scope.invalidKeyOrPassword = false;
});
$scope.$watch('selectedMosaic', function(){
$scope.txMosaicSupplyData.mosaic = $scope.walletScope.mosaicOwned[$scope.currentAccount][$scope.selectedMosaic].mosaicId;
});
$scope.updateCurrentAccountMosaics();
$scope.okPressed = false;
$scope.ok = function txMosaicSupplyOk() {
$scope.okPressed = true;
$timeout(function txMosaicSupplyDeferred(){
$scope._ok().then(function(){
$scope.okPressed = false;
}, function(){
$scope.okPressed = false;
});
});
};
$scope._ok = function txMosaicSupply_Ok() {
var orig = $scope.storage.getObject('txMosaicSupplyDefaults');
$.extend(orig, {
'due': $scope.txMosaicSupplyData.due,
'isMultisig': $scope.txMosaicSupplyData.isMultisig,
});
$scope.storage.setObject('txMosaicSupplyDefaults', orig);
var rememberedKey = $scope.walletScope.sessionData.getRememberedKey();
if (rememberedKey) {
$scope.common.privatekey = CryptoHelpers.decrypt(rememberedKey);
} else {
if (! CryptoHelpers.passwordToPrivatekey($scope.common, $scope.walletScope.networkId, $scope.walletScope.walletAccount) ) {
$scope.invalidKeyOrPassword = true;
return $q.resolve(0);
}
}
var entity = Transactions.prepareMosaicSupply($scope.common, $scope.txMosaicSupplyData);
return Transactions.serializeAndAnnounceTransaction(entity, $scope.common, $scope.txMosaicSupplyData, $scope.walletScope.nisPort,
function(data) {
if (data.status === 200) {
if (data.data.code >= 2) {
alert('failed when trying to send tx: ' + data.data.message);
} else {
$scope.$close();
}
if (rememberedKey) { delete $scope.common.privatekey; }
}
},
function(operation, data) {
// will do for now, will change it to modal later
alert('failed at '+operation + " " + data.data.error + " " + data.data.message);
if (rememberedKey) { delete $scope.common.privatekey; }
}
);
}; // $scope._ok
$scope.cancel = function () {
$scope.$dismiss();
};
}
]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/CryptoHelpers',
'sinks',
'filters/filters',
'services/Transactions'
], function(angular, $, CryptoHelpers, sinks) {
var mod = angular.module('walletApp.controllers');
mod.controller('TxNamespaceCtrl',
["$scope", "$window", "$q", "$timeout", "Transactions", 'walletScope',
function($scope, $window, $q, $timeout, Transactions, walletScope) {
$scope.walletScope = walletScope;
$scope.storage = $window.localStorage;
$scope.storage.setDefault('txNamespaceDefaults', {});
// begin tracking currently selected account
$scope._updateCurrentAccount = function() {
var acct = $scope.walletScope.accountData.account.address
if ($scope.txNamespaceData.isMultisig) {
acct = $scope.txNamespaceData.multisigAccount.address;
}
$scope.currentAccount = acct;
};
$scope.selectTab = function selectTab(v) {
if (v === 'multisig') {
$scope.txNamespaceData.isMultisig = true;
} else {
$scope.txNamespaceData.isMultisig = false;
}
$scope._updateCurrentAccount();
};
// end begin tracking currently selected account
// load data from storage
$scope.common = {
'requiresKey': $scope.walletScope.sessionData.getRememberedKey() === undefined,
'password': '',
'privatekey': '',
};
$scope.txNamespaceData = {
'rentalFeeSink': '',
'rentalFee': 0,
'namespaceName': '',
'namespaceParent': null,
'fee': 0,
'innerFee': 0,
'due': $scope.storage.getObject('txNamespaceDefaults').due || 60,
'isMultisig': ($scope.storage.getObject('txNamespaceDefaults').isMultisig && walletScope.accountData.meta.cosignatoryOf.length > 0) || false,
'multisigAccount': walletScope.accountData.meta.cosignatoryOf.length == 0?'':walletScope.accountData.meta.cosignatoryOf[0]
};
$scope.txNamespaceData.rentalFeeSink = sinks.namespace[$scope.walletScope.networkId];
$scope.namespaceLevel3 = function(elem) {
return elem.fqn.split('.').length < 3
};
function updateFee() {
var entity = Transactions.prepareNamespace($scope.common, $scope.txNamespaceData);
$scope.txNamespaceData.fee = entity.fee;
if ($scope.txNamespaceData.isMultisig) {
$scope.txNamespaceData.innerFee = entity.otherTrans.fee;
}
}
$scope.$watchGroup(['txNamespaceData.namespaceName', 'txNamespaceData.namespaceParent', 'txNamespaceData.isMultisig'], function(nv, ov){
updateFee();
});
$scope.$watchGroup(['common.password', 'common.privatekey'], function(nv,ov){
$scope.invalidKeyOrPassword = false;
});
$scope.$watch('txNamespaceData.namespaceParent', function(nv, ov){
if ($scope.txNamespaceData.namespaceParent) {
$scope.txNamespaceData.rentalFee = 5000 * 1000000;
} else {
$scope.txNamespaceData.rentalFee = 50000 * 1000000;
}
});
$scope._updateCurrentAccount();
$scope.okPressed = false;
$scope.ok = function txNamespaceOk() {
$scope.okPressed = true;
$timeout(function txNamespaceDeferred(){
$scope._ok().then(function(){
$scope.okPressed = false;
}, function(){
$scope.okPressed = false;
});
});
};
$scope._ok = function txNamespace_Ok() {
var orig = $scope.storage.getObject('txNamespaceDefaults');
$.extend(orig, {
'due': $scope.txNamespaceData.due,
'isMultisig': $scope.txNamespaceData.isMultisig,
});
$scope.storage.setObject('txNamespaceDefaults', orig);
var rememberedKey = $scope.walletScope.sessionData.getRememberedKey();
if (rememberedKey) {
$scope.common.privatekey = CryptoHelpers.decrypt(rememberedKey);
} else {
if (! CryptoHelpers.passwordToPrivatekey($scope.common, $scope.walletScope.networkId, $scope.walletScope.walletAccount) ) {
$scope.invalidKeyOrPassword = true;
return $q.resolve(0);
}
}
var entity = Transactions.prepareNamespace($scope.common, $scope.txNamespaceData);
return Transactions.serializeAndAnnounceTransaction(entity, $scope.common, $scope.txNamespaceData, $scope.walletScope.nisPort,
function(data) {
if (data.status === 200) {
if (data.data.code >= 2) {
alert('failed when trying to send tx: ' + data.data.message);
} else {
$scope.$close();
}
}
if (rememberedKey) { delete $scope.common.privatekey; }
},
function(operation, data) {
// will do for now, will change it to modal later
alert('failed at '+operation + " " + data.data.error + " " + data.data.message);
if (rememberedKey) { delete $scope.common.privatekey; }
}
);
}; // $scope._ok
$scope.cancel = function () {
$scope.$dismiss();
};
}
]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/CryptoHelpers',
'utils/Address',
'filters/filters',
'services/Transactions'
], function(angular, $, CryptoHelpers, Address) {
var mod = angular.module('walletApp.controllers');
mod.controller('TxTransferCtrl',
["$scope", "$window", "$http", "$q", "$timeout", "Transactions", 'walletScope',
function($scope, $window, $http, $q, $timeout, Transactions, walletScope) {
$scope.walletScope = walletScope;
$scope.encryptDisabled = false;
$scope.storage = $window.localStorage;
$scope.storage.setDefault('txTransferDefaults', {});
// load data from storage
$scope.common = {
'requiresKey': $scope.walletScope.sessionData.getRememberedKey() === undefined,
'password': '',
'privatekey': '',
};
$scope.txTransferData = {
'recipient': $scope.storage.getObject('txTransferDefaults').recipient || '',
'amount': $scope.storage.getObject('txTransferDefaults').amount,
'fee': $scope.storage.getObject('txTransferDefaults').fee || 0,
'innerFee': 0,
'due': $scope.storage.getObject('txTransferDefaults').due || 60,
'message': $scope.storage.getObject('txTransferDefaults').message || '',
'encryptMessage': $scope.storage.getObject('txTransferDefaults').encryptMessage || false,
'isMultisig': ($scope.storage.getObject('txTransferDefaults').isMultisig && walletScope.accountData.meta.cosignatoryOf.length > 0) || false,
'multisigAccount': walletScope.accountData.meta.cosignatoryOf.length == 0?'':walletScope.accountData.meta.cosignatoryOf[0]
};
function updateFee() {
var entity = Transactions.prepareTransfer($scope.common, $scope.txTransferData);
$scope.txTransferData.fee = entity.fee;
if ($scope.txTransferData.isMultisig) {
$scope.txTransferData.innerFee = entity.otherTrans.fee;
}
}
$scope.$watchGroup(['txTransferData.amount', 'txTransferData.message', 'txTransferData.isMultisig'], function(nv, ov){
updateFee();
if ($scope.txTransferData.isMultisig) {
$scope.txTransferData.encryptMessage = false;
$scope.encryptDisabled = true;
} else {
$scope.encryptDisabled = false;
}
});
$scope.$watchGroup(['common.password', 'common.privatekey'], function(nv,ov){
$scope.invalidKeyOrPassword = false;
});
$scope.recipientCache = {};
$scope.$watch('txTransferData.recipient', function(nv, ov){
if (! nv) {
return;
}
var recipientAddress = nv.toUpperCase().replace(/-/g, '');
var nisPort = $scope.walletScope.nisPort;
var obj = {'params':{'address':recipientAddress}};
if (! (recipientAddress in $scope.recipientCache)) {
var _uriParser = document.createElement('a');
_uriParser.href = $scope.walletScope.sessionData.getNode().uri;
if (Address.isFromNetwork(recipientAddress, $scope.walletScope.networkId)) {
console.log(recipientAddress, $scope.walletScope.networkId);
$http.get('http://'+_uriParser.hostname+':'+nisPort+'/account/get', obj).then(function (data){
$scope.recipientCache[recipientAddress] = data.data.account.publicKey;
});
}
}
});
$scope.okPressed = false;
$scope.ok = function txTransferOk() {
$scope.okPressed = true;
$timeout(function txTransferDeferred(){
$scope._ok().then(function(){
$scope.okPressed = false;
}, function(){
$scope.okPressed = false;
});
}); // timeout
};
$scope._ok = function txTransfer_Ok() {
// save most recent data
// BUG: tx data is saved globally not per wallet...
var orig = $scope.storage.getObject('txTransferDefaults');
$.extend(orig, {
'recipient':$scope.txTransferData.recipient,
'amount':$scope.txTransferData.amount,
'fee':$scope.txTransferData.fee,
'due':$scope.txTransferData.due,
'message':$scope.txTransferData.message,
'encryptMessage':$scope.txTransferData.encryptMessage,
'isMultisig':$scope.txTransferData.isMultisig,
});
$scope.storage.setObject('txTransferDefaults', orig);
var recipientAddress = $scope.txTransferData.recipient.toUpperCase().replace(/-/g, '');
$scope.txTransferData.recipientPubKey = $scope.recipientCache[recipientAddress];
if ($scope.txTransferData.encryptMessage && !$scope.txTransferData.recipientPubKey) {
return $scope.walletScope.displayWarning("Encrypted message selected, but couldn't find public key of a recipient");
}
var rememberedKey = $scope.walletScope.sessionData.getRememberedKey();
if (rememberedKey) {
$scope.common.privatekey = CryptoHelpers.decrypt(rememberedKey);
} else {
if (! CryptoHelpers.passwordToPrivatekey($scope.common, $scope.walletScope.networkId, $scope.walletScope.walletAccount) ) {
$scope.invalidKeyOrPassword = true;
return $q.resolve(0);
}
}
var entity = Transactions.prepareTransfer($scope.common, $scope.txTransferData);
return Transactions.serializeAndAnnounceTransaction(entity, $scope.common, $scope.txTransferData, $scope.walletScope.nisPort,
function(data) {
if (data.status === 200) {
if (data.data.code >= 2) {
alert('failed when trying to send tx: ' + data.data.message);
} else {
$scope.$close();
}
}
if (rememberedKey) { delete $scope.common.privatekey; }
},
function(operation, data) {
// will do for now, will change it to modal later
alert('failed at '+operation + " " + data.data.error + " " + data.data.message);
if (rememberedKey) { delete $scope.common.privatekey; }
}
);
}; // $scope._ok
$scope.cancel = function () {
$scope.$dismiss();
};
}
]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/CryptoHelpers',
'filters/filters',
'services/Transactions'
], function(angular, $, CryptoHelpers) {
var mod = angular.module('walletApp.controllers');
mod.controller('TxTransferV2Ctrl',
["$scope", "$window", "$q", "$timeout", "Transactions", 'walletScope',
function($scope, $window, $q, $timeout, Transactions, walletScope) {
$scope.walletScope = walletScope;
$scope.counter = 1;
$scope.storage = $window.localStorage;
$scope.storage.setDefault('txTransfer2Defaults', {});
// begin tracking currently selected account and it's mosaics
$scope._updateCurrentAccount = function() {
var acct = $scope.walletScope.accountData.account.address
if ($scope.txTransferV2Data.isMultisig) {
acct = $scope.txTransferV2Data.multisigAccount.address;
}
$scope.currentAccount = acct;
};
$scope.selectTab = function selectTab(v) {
if (v === 'multisig') {
$scope.txTransferV2Data.isMultisig = true;
} else {
$scope.txTransferV2Data.isMultisig = false;
}
$scope.updateCurrentAccountMosaics();
};
$scope.updateCurrentAccountMosaics = function updateCurrentAccountMosaics() {
$scope._updateCurrentAccount();
var acct = $scope.currentAccount;
$scope.currentAccountMosaicNames = Object.keys($scope.walletScope.mosaicOwned[acct]).sort();
$scope.selectedMosaic = "nem:xem";
};
// end begin tracking currently selected account and it's mosaics
$scope.removeMosaic = function removeMosaic(index) {
$scope.txTransferV2Data.mosaics.splice(index, 1);
};
function mosaicIdToName(mosaicId) {
return mosaicId.namespaceId + ":" + mosaicId.name;
}
$scope.attachMosaic = function attachMosaic() {
var acct = $scope.currentAccount;
var mosaic = $scope.walletScope.mosaicOwned[acct][$scope.selectedMosaic];
var elem = $.grep($scope.txTransferV2Data.mosaics, function(w){ return mosaicIdToName(mosaic.mosaicId) === mosaicIdToName(w.mosaicId); });
if (elem.length === 0) {
$scope.counter += 1;
$scope.txTransferV2Data.mosaics.push({'mosaicId':mosaic['mosaicId'], 'quantity':0, 'gid':'mos_id_'+$scope.counter});
} else {
$('#'+elem[0].gid).focus();
}
};
// load data from storage
$scope.common = {
'requiresKey': $scope.walletScope.sessionData.getRememberedKey() === undefined,
'password': '',
'privatekey': '',
};
$scope.txTransferV2Data = {
'recipient': $scope.storage.getObject('txTransfer2Defaults').recipient || '',
'multiplier': $scope.storage.getObject('txTransfer2Defaults').multiplier || 1,
'amount': 0,
'fee': $scope.storage.getObject('txTransfer2Defaults').fee || 0,
'innerFee': 0,
'due': $scope.storage.getObject('txTransfer2Defaults').due || 60,
'message': $scope.storage.getObject('txTransfer2Defaults').message || '',
'isMultisig': ($scope.storage.getObject('txTransfer2Defaults').isMultisig && walletScope.accountData.meta.cosignatoryOf.length > 0) || false,
'multisigAccount': walletScope.accountData.meta.cosignatoryOf.length == 0?'':walletScope.accountData.meta.cosignatoryOf[0],
'mosaics': [ {'mosaicId':{'namespaceId':'nem', 'name':'xem'}, 'quantity':0, 'gid':'mos_id_0'} ]
};
function updateFee() {
var entity = Transactions.prepareTransferV2($scope.common, $scope.walletScope.mosaicDefinitionMetaDataPair, $scope.txTransferV2Data);
$scope.txTransferV2Data.fee = entity.fee;
if ($scope.txTransferV2Data.isMultisig) {
$scope.txTransferV2Data.innerFee = entity.otherTrans.fee;
}
}
$scope.$watchGroup(['txTransferV2Data.message', 'txTransferV2Data.isMultisig'], function(nv, ov){
updateFee();
});
$scope.$watchGroup(['common.password', 'common.privatekey'], function(nv,ov){
$scope.invalidKeyOrPassword = false;
});
$scope.$watch('txTransferV2Data.mosaics', function(){
updateFee();
}, true);
$scope.$watch('multiplier', function(){
$scope.txTransferV2Data.amount = parseInt($scope.txTransferV2Data.multiplier * 1000000, 10) || 0;
});
$scope.updateCurrentAccountMosaics();
$scope.okPressed = false;
$scope.ok = function txTransferV2Ok() {
$scope.okPressed = true;
$timeout(function txTransferV2Deferred(){
$scope._ok().then(function(){
$scope.okPressed = false;
}, function(){
$scope.okPressed = false;
});
});
};
$scope._ok = function txTransferV2_Ok() {
// save most recent data
// BUG: tx data is saved globally not per wallet...
var orig = $scope.storage.getObject('txTransfer2Defaults');
$.extend(orig, {
'recipient': $scope.txTransferV2Data.recipient,
'multiplier': $scope.txTransferV2Data.multiplier,
'fee': $scope.txTransferV2Data.fee,
'due': $scope.txTransferV2Data.due,
'message': $scope.txTransferV2Data.message,
'isMultisig': $scope.txTransferV2Data.isMultisig,
});
$scope.storage.setObject('txTransfer2Defaults', orig);
//
var rememberedKey = $scope.walletScope.sessionData.getRememberedKey();
if (rememberedKey) {
$scope.common.privatekey = CryptoHelpers.decrypt(rememberedKey);
} else {
if (! CryptoHelpers.passwordToPrivatekey($scope.common, $scope.walletScope.networkId, $scope.walletScope.walletAccount) ) {
$scope.invalidKeyOrPassword = true;
return $q.resolve(0);
}
}
var entity = Transactions.prepareTransferV2($scope.common, $scope.walletScope.mosaicDefinitionMetaDataPair, $scope.txTransferV2Data);
return Transactions.serializeAndAnnounceTransaction(entity, $scope.common, $scope.txTransferV2Data, $scope.walletScope.nisPort,
function(data) {
if (data.status === 200) {
if (data.data.code >= 2) {
alert('failed when trying to send tx: ' + data.data.message);
} else {
$scope.$close();
}
}
if (rememberedKey) { delete $scope.common.privatekey; }
},
function(operation, data) {
// will do for now, will change it to modal later
alert('failed at '+operation + " " + data.data.error + " " + data.data.message);
if (rememberedKey) { delete $scope.common.privatekey; }
}
);
}; // $scope._ok
$scope.cancel = function () {
$scope.$dismiss();
};
}
]);
});
'use strict';
define([
'definitions',
'jquery',
'utils/Connector',
'utils/CryptoHelpers',
'utils/KeyPair',
'utils/TransactionType',
// angular related
'controllers/dialogWarning',
'controllers/txTransfer',
'controllers/txTransferV2',
'controllers/txNamespace',
'controllers/txMosaic',
'controllers/txMosaicSupply',
'controllers/txCosignature',
'controllers/txDetails',
'controllers/msgDecode',
'filters/filters',
'services/Transactions',
'services/SessionData'
], function(angular, $, Connector, CryptoHelpers, KeyPair, TransactionType) {
var mod = angular.module('walletApp.controllers');
mod.controller('WalletCtrl',
["$scope", "$http", "$location", "$window", "$timeout", "$routeParams", "$uibModal", "sessionData",
function WalletCtrl($scope, $http, $location, $window, $timeout, $routeParams, $uibModal, sessionData) {
if (sessionData.getNisPort() === 0 || !sessionData.getNetworkId() || !sessionData.getNode()) {
$location.path('/login');
return;
}
$scope.$on('$locationChangeStart', function( event ) {
if ($scope.connector) {
sessionData.setRememberedKey(undefined);
$scope.connector.close();
}
});
$scope.connector = undefined;
$scope.storage = $window.localStorage;
$scope.storage.setDefault('txTransferDefaults', {});
var elem = $.grep($scope.storage.getObject('wallets'), function(w){ return w.name == $routeParams.walletName; });
$scope.walletAccount = elem.length == 1 ? elem[0].accounts[0] : null;
$scope.nisPort = sessionData.getNisPort();
$scope.networkId = sessionData.getNetworkId();
$scope.nisHeight = 0;
$scope.sessionData = sessionData;
$scope.activeWalletTab = 0;
$scope.setWalletTab = function setWalletTab(index) {
$scope.activeWalletTab = index;
};
function mosaicIdToName(mosaicId) {
return mosaicId.namespaceId + ":" + mosaicId.name;
}
// ==== ==== ==== ==== dialogs
// in case of dialogs, I'm passing the scope, we could ofc use $scope.$parent,
// in descendant controllers, but this makes it more verbose and easier to follow
// it's also easier, than passing proper elements from current scope.
$scope.displayWarning = function(warningMsg) {
var modalInstance = $uibModal.open({
animation: true,
templateUrl: 'views/dialogWarning.html',
controller: 'DialogWarningCtrl',
backdrop: true,
resolve: {
warningMsg: function() { return warningMsg; }
}
});
return modalInstance.result;
};
$scope.displayTransferDialog = function() {
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/txTransfer.html',
controller: 'TxTransferCtrl',
backdrop: false,
resolve: {
walletScope: function() {
return $scope;
}
}
});
};
$scope.displayTransferV2Dialog = function() {
if ($scope.networkId === 104 && $scope.nisHeight < 440000) {
$scope.displayWarning("v2 transfers will be available after fork at 440k");
return;
}
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/txTransferV2.html',
controller: 'TxTransferV2Ctrl',
backdrop: false,
resolve: {
walletScope: function() {
return $scope;
}
}
});
};
$scope.displayNamespaceDialog = function() {
if ($scope.networkId === 104 && $scope.nisHeight < 440000) {
$scope.displayWarning("namespaces will be available after fork at 440k");
return;
}
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/txNamespace.html',
controller: 'TxNamespaceCtrl',
backdrop: false,
resolve: {
walletScope: function() {
return $scope;
}
}
});
};
$scope.displayMosaicDialog = function() {
if ($scope.networkId === 104 && $scope.nisHeight < 440000) {
$scope.displayWarning("mosaics will be available after fork at 440k");
return;
}
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/txMosaic.html',
controller: 'TxMosaicCtrl',
backdrop: false,
resolve: {
walletScope: function() {
return $scope;
}
}
});
};
$scope.displayMosaicSupplyDialog = function() {
if ($scope.networkId === 104 && $scope.nisHeight < 440000) {
$scope.displayWarning("v2 transfers will be available after fork at 440k");
return;
}
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/txMosaicSupply.html',
controller: 'TxMosaicSupplyCtrl',
backdrop: false,
resolve: {
walletScope: function() {
return $scope;
}
}
});
};
$scope.displayDecodeMessage = function(tx) {
if (!tx || !tx.message || tx.message.type !== 2) {
alert("missing transaction data");
return;
}
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/msgDecode.html',
controller: 'MsgDecodeCtrl',
backdrop: false,
size: 'lg',
resolve: {
walletScope: function() {
return $scope;
},
tx: function() {
return tx;
}
}
});
};
$scope.cosignTransaction = function(parentTx, tx, meta) {
//console.log("cosignTransaction parent", parentTx, "\ncosignTransaction inner", tx, "\ncosignTransaction meta", meta);
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/txCosignature.html',
controller: 'TxCosignatureCtrl',
backdrop: false,
resolve: {
walletScope: function() {
return $scope;
},
parent: function() {
return parentTx;
},
meta: function() {
return meta;
}
}
});
};
$scope.displayTransactionDetails = function(parentTx, tx, meta) {
var modalInstance = $uibModal.open({
animation: false,
templateUrl: 'views/txDetails.html',
controller: 'TxDetailsCtrl',
backdrop: false,
size: 'lg',
resolve: {
walletScope: function() {
return $scope;
},
parent: function() {
return parentTx;
},
tx: function() {
return tx;
},
meta: function() {
return meta;
}
}
});
};
// ==== ==== ==== ==== cached data
$scope.accountData = {};
$scope.transactions = [];
$scope.unconfirmedSize = 0;
$scope.unconfirmed = {};
$scope.mosaicDefinitionMetaDataPair = {};
$scope.mosaicDefinitionMetaDataPairSize = 0;
$scope.mosaicOwned = {};
$scope.mosaicOwnedSize = {};
$scope.namespaceOwned = {};
$scope.getLevy = function getLevy(d) {
var mosaicName = mosaicIdToName(d.mosaicId);
if (!(mosaicName in $scope.mosaicDefinitionMetaDataPair)) {
return false;
}
var mosaicDefinitionMetaDataPair = $scope.mosaicDefinitionMetaDataPair[mosaicName];
return mosaicDefinitionMetaDataPair.mosaicDefinition.levy;
};
$scope.mosaicIdToName = mosaicIdToName;
// ==== ==== ==== ==== connection to the server and websocket handlers
// if we got wallet name let's set up everything...
if (elem.length == 1) {
$scope.name = elem[0].name;
$scope.account = elem[0].accounts[0].address;
$scope.connectionStatus = "connecting";
var _uriParser = document.createElement('a');
_uriParser.href = $scope.sessionData.getNode().uri;
$http.get('http://'+_uriParser.hostname+':'+$scope.nisPort+'/chain/height').then(function (data){
$scope.nisHeight = data.height;
});
var connector = Connector(sessionData.getNode(), elem[0].accounts[0].address);
connector.connect(function(){
$timeout(function(){
$scope.connectionStatus = "connected";
}, 0);
function unconfirmedCallback(d) {
// we could first check if coming tx is already in unconfirmed, but
// tx itself can change in case of multisig txes,, so don't do that check
$timeout(function() {
$scope.unconfirmed[d.meta.hash.data] = d;
$scope.unconfirmedSize = Object.keys($scope.unconfirmed).length;
}, 0);
//console.log("unconfirmed data: ", Object.keys($scope.unconfirmed).length, d);
var audio = new Audio('/lightwallet/ding.ogg');
audio.play();
}
function confirmedCallback(d) {
$timeout(function() {
delete $scope.unconfirmed[d.meta.hash.data];
$scope.unconfirmedSize = Object.keys($scope.unconfirmed).length;
$scope.transactions.push(d);
}, 0);
// console.log(">> transactions data: ", d);
var audio = new Audio('/lightwallet/ding2.ogg');
audio.play();
}
function mosaicDefinitionCallback(d) {
$timeout(function() {
$scope.mosaicDefinitionMetaDataPair[mosaicIdToName(d.mosaicDefinition.id)] = d;
$scope.mosaicDefinitionMetaDataPairSize = Object.keys($scope.mosaicDefinitionMetaDataPair).length;
}, 0);
}
function mosaicCallback(d, address) {
$timeout(function() {
var mosaicName = mosaicIdToName(d.mosaicId);
if (! (address in $scope.mosaicOwned)) {
$scope.mosaicOwned[address] = {};
}
$scope.mosaicOwned[address][mosaicName] = d;
$scope.mosaicOwnedSize[address] = Object.keys($scope.mosaicOwned[address]).length;
}, 0);
}
function namespaceCallback(d, address) {
$timeout(function() {
var namespaceName = d.fqn;
if (! (address in $scope.namespaceOwned)) {
$scope.namespaceOwned[address] = {};
}
$scope.namespaceOwned[address][namespaceName] = d;
}, 0);
}
connector.on('errors', function(name, d) {
console.log(d);
alert(d.error + " " + d.message);
});
connector.on('account', function(d) {
$timeout(function(){
$scope.accountData = d;
//console.log("account data: ", $scope.accountData);
// prepare callback for multisig accounts
for (var elem of $scope.accountData.meta.cosignatoryOf) {
connector.onConfirmed(confirmedCallback, elem.address);
connector.onUnconfirmed(unconfirmedCallback, elem.address);
connector.onNamespace(namespaceCallback, elem.address);
connector.onMosaicDefinition(mosaicDefinitionCallback, elem.address);
connector.onMosaic(mosaicCallback, elem.address);
}
// we need to subscribe to multisig accounts, in order to receive notifications
// about transactions involving those accounts
for (var elem of $scope.accountData.meta.cosignatoryOf) {
// no need to check return value, as if we're here, it means we're already connected
connector.subscribeToMultisig(elem.address);
// we don't need to request that, as request for accounts' unconfirmed txes should include those needed cosingature
//connector.requestUnconfirmedTransactions(elem.address);
connector.requestAccountNamespaces(elem.address);
connector.requestAccountMosaicDefinitions(elem.address);
connector.requestAccountMosaics(elem.address);
}
}, 0);
});
connector.on('recenttransactions', function(d) {
d.data.reverse();
$timeout(function(){
$scope.transactions = d.data;
}, 0);
//console.log("recenttransactions data: ", d);
});
connector.on('newblocks', function(blockHeight) {
$scope.nisHeight = blockHeight.height;
var cleanedTransactions = [];
$.each($scope.transactions, function(idx, tx) {
if (tx.meta.height < blockHeight.height) {
cleanedTransactions.push(tx);
} else {
//console.log("OK, ", blockHeight, "removed tx: ", tx);
}
});
$timeout(function(){
$scope.transactions = cleanedTransactions;
}, 0);
});
connector.onConfirmed(confirmedCallback);
connector.onUnconfirmed(unconfirmedCallback);
connector.onNamespace(namespaceCallback);
connector.onMosaicDefinition(mosaicDefinitionCallback);
connector.onMosaic(mosaicCallback);
connector.requestAccountData();
connector.requestAccountNamespaces();
connector.requestAccountMosaicDefinitions();
connector.requestAccountTransactions();
connector.requestAccountMosaics();
});
$scope.connector = connector;
}
}]
);
function txTypeToName(id) {
switch (id) {
case TransactionType.Transfer: return 'Transfer';
case TransactionType.ImportanceTransfer: return 'ImportanceTransfer';
case TransactionType.MultisigModification: return 'MultisigModification';
case TransactionType.ProvisionNamespace: return 'ProvisionNamespace';
case TransactionType.MosaicDefinition: return 'MosaicDefinition';
case TransactionType.MosaicSupply: return 'MosaicSupply';
default: return 'Unknown_'+id;
}
}
function needsSignature(multisigTransaction, accountData) {
// we're issuer
if (multisigTransaction.transaction.signer === accountData.account.publicKey) {
return false;
}
// check if we're already on list of signatures
for (var elem of multisigTransaction.transaction.signatures) {
if (elem.signer === accountData.account.publicKey) {
return false;
}
}
return true;
}
mod.directive('tagtransaction', function() {
return {
restrict: 'E',
scope: {
d: '=',
tooltipPosition: '=',
accountData: '=',
},
// we're passing ng-include as a template in order to dynamically select proper templates,
// the selection itself is done below in assignment to scope.templateUri
template: '<ng-include src="templateUri"/>',
link: function postLink(scope) {
if (scope.d.transaction.type === 4100) {
scope.tx = scope.d.transaction.otherTrans;
scope.meta = scope.d.meta;
scope.parent = scope.d.transaction;
} else {
scope.tx = scope.d.transaction;
scope.meta = scope.d.meta;
scope.parent = undefined;
}
scope.confirmed = !(scope.meta.height === Number.MAX_SAFE_INTEGER);
// if multisig and not confirmed, check if we need to cosign
scope.needsSignature = scope.parent && !scope.confirmed && scope.accountData && needsSignature(scope.d, scope.accountData);
scope.templateName = txTypeToName(scope.tx.type);
scope.templateUri = 'views/line'+scope.templateName+'.html';
scope.cosignCallback = scope.$parent.cosignTransaction;
scope.displayTransactionDetails = scope.$parent.displayTransactionDetails;
scope.networkId = scope.$parent.networkId;
}
};
});
mod.directive('tagdetails', ["$http", function($http) {
return {
restrict: 'E',
scope: {
parent: '=',
tx: '=',
meta: '='
},
template: '<ng-include src="templateUri"/>',
link: function postLink(scope) {
scope.transactionTypeName = txTypeToName(scope.tx.type);
scope.templateUri = 'views/details'+scope.transactionTypeName+'.html';
scope.decode = function(tx) {
if (tx.message && tx.message.type === 2) {
scope.$parent.walletScope.displayDecodeMessage(tx);
}
};
// scope.$parent == TxDetailsCtrl
scope.mosaicDefinitionMetaDataPair = scope.$parent.walletScope.mosaicDefinitionMetaDataPair;
scope.getLevy = scope.$parent.walletScope.getLevy;
scope.mosaicIdToName = scope.$parent.walletScope.mosaicIdToName;
scope.networkId = scope.$parent.walletScope.networkId;
scope.recipientPublicKey = '';
scope.gettingRecipientInfo = true;
scope.requiresKey = scope.$parent.walletScope.sessionData.getRememberedKey() === undefined;
if (!scope.requiresKey && scope.tx.type === TransactionType.Transfer && scope.tx.message && scope.tx.message.type === 2) {
var nisPort = scope.$parent.walletScope.nisPort;
var obj = {'params':{'address':scope.tx.recipient}};
var _uriParser = document.createElement('a');
_uriParser.href = scope.$parent.walletScope.sessionData.getNode().uri;
$http.get('http://'+_uriParser.hostname+':'+nisPort+'/account/get', obj).then(function (data){
scope.recipientPublicKey = data.data.account.publicKey;
var privateKey = CryptoHelpers.decrypt(scope.$parent.walletScope.sessionData.getRememberedKey());
var kp = KeyPair.create(privateKey);
if (kp.publicKey.toString() === scope.tx.signer) {
// sender
var privateKey = privateKey;
var publicKey = scope.recipientPublicKey;
} else {
var privateKey = privateKey;
var publicKey = scope.tx.signer;
}
var payload = scope.tx.message.payload;
scope.decoded = {'type':1, 'payload':CryptoHelpers.decode(privateKey, publicKey, payload) };
scope.gettingRecipientInfo = false;
}, function(data) {
alert("couldn't obtain data from nis server");
console.log("couldn't obtain data from nis server", scope.tx.recipient);
scope.gettingRecipientInfo = false;
});
}
}
};
}]);
mod.directive('taglevy', function(){
return {
restrict: 'E',
scope: {
mos: '=',
tx: '=',
mosaics: '='
},
template: '',
transclude: true,
compile: function compile(tElement, tAttrs, transclude) {
return function postLink(scope, element, attrs) {
function mosaicIdToName(mosaicId) {
if (! mosaicId) return mosaicId;
return mosaicId.namespaceId + ":" + mosaicId.name;
}
function getLevy(d) {
if (!scope.mosaics) return undefined;
var mosaicName = mosaicIdToName(d.mosaicId);
if (!(mosaicName in scope.mosaics)) {
return undefined;
}
var mosaicDefinitionMetaDataPair = scope.mosaics[mosaicName];
return mosaicDefinitionMetaDataPair.mosaicDefinition.levy;
}
scope.levy = getLevy(scope.mos);
var foo = scope;
scope.$watch('mosaics', function(nv, ov) {
scope.levy = getLevy(scope.mos);
//console.log('rerender', Object.keys(scope.mosaics).length, mosaicIdToName(scope.mos.mosaicId), mosaicIdToName(scope.levy ? scope.levy.mosaicId : undefined));
}, true);
transclude(scope, function(clone, scope) {
element.append(clone);
});
};
}
};
});
mod.directive('title', function() {
return {
link: function($scope, element, attrs) {
var watch = $scope.$watch(function() {
return element.children().length;
}, function() {
$scope.$evalAsync(function() {
$('[data-toggle="tooltip"]').tooltip();
});
});
},
};
});
return mod;
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment