safe-coins-wallet
Version:
SAFE coin/altcoins wallet implementation
616 lines (541 loc) • 21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/*
Module which takes care of the SAFE Network altcoins wallet functioning
*/
var crypto = require('crypto');
var ACTION_INSERT = 'Insert';
var TYPE_TAG_WALLET = 1012017;
var TYPE_TAG_WALLET_TX_INBOX = 20082018;
var TYPE_TAG_THANKS_COIN = 21082018;
var COIN_ENTRY_KEY_DATA = 'coin-data';
var TX_INBOX_ENTRY_KEY_ENC_PK = '__tx_enc_pk';
var TX_INBOX_ENTRY_KEY_WALLET_PK = '__wallet_pk';
var TX_INBOX_METADATA_NAME = 'ThanksCoins TX Inbox';
var TX_INBOX_METADATA_DESC = 'Container to receive notifications of ThanksCoins transactions';
var WALLET_ENTRY_KEY_COINS = '__coins';
var WALLET_METADATA_NAME = 'ThanksCoins Wallet';
var WALLET_METADATA_DESC = 'Container to store the list of ThanksCoins addresses owned';
var ENTRY_KEY_MD_METADATA = '_metadata';
// Generic Helper functions
var _fromArrayBuffer = function _fromArrayBuffer(buf) {
return String.fromCharCode.apply(null, new Uint8Array(buf));
};
var _genXorName = function _genXorName(safeApp, str) {
return safeApp.crypto.sha3Hash(str || '');
};
// Altcoin Wallet management functions
var _readEncryptedEntry = function _readEncryptedEntry(md, key) {
var encKey, encValue;
return Promise.resolve().then(function () {
return md.encryptKey(key);
}).then(function (_resp) {
encKey = _resp;
return md.get(encKey);
}).then(function (_resp) {
encValue = _resp;
return md.decrypt(encValue.buf);
});
};
var _insertEntriesEncrypted = function _insertEntriesEncrypted(safeApp, md, data) {
function _recursive() {
if (i < keys.length) {
return Promise.resolve().then(function () {
key = keys[i];
return md.encryptKey(key);
}).then(function (_resp) {
encKey = _resp;
return md.encryptValue(data[key]);
}).then(function (_resp) {
encValue = _resp;
return mutations.insert(encKey, encValue);
}).then(function () {
i++;
return _recursive();
});
}
}
var mutations, keys, i, key, encKey, encValue;
return Promise.resolve().then(function () {
return safeApp.mutableData.newMutation();
}).then(function (_resp) {
mutations = _resp;
keys = Object.keys(data);
i = 0;
return _recursive();
}).then(function () {
return md.applyEntriesMutation(mutations);
}).then(function () {});
};
var createWallet = function createWallet(safeApp, pk) {
var emptyCoins, encKeyPair, secEncKey, nonce, xorName, walletMd, serialisedWallet, walletArr;
return Promise.resolve().then(function () {
console.log("Creating the coin wallet...");
emptyCoins = _defineProperty({}, WALLET_ENTRY_KEY_COINS, JSON.stringify([]));
return safeApp.crypto.generateEncKeyPair();
}).then(function (_resp) {
encKeyPair = _resp;
return encKeyPair.secEncKey.getRaw();
}).then(function (_resp) {
secEncKey = _resp;
return safeApp.crypto.generateNonce();
}).then(function (_resp) {
nonce = _resp;
return _genXorName(safeApp, pk);
}).then(function (_resp) {
xorName = _resp;
return safeApp.mutableData.newPrivate(xorName, TYPE_TAG_WALLET, secEncKey, nonce);
}).then(function (_resp) {
walletMd = _resp; // TODO: support the case that it exists already
return walletMd.quickSetup({}, WALLET_METADATA_NAME, WALLET_METADATA_DESC);
}).then(function () {
return _insertEntriesEncrypted(safeApp, walletMd, emptyCoins);
}).then(function () {
return walletMd.serialise();
}).then(function (_resp) {
serialisedWallet = _resp;
walletArr = new Uint8Array(serialisedWallet);
return walletArr.toString();
});
};
var _deserialiseArray = function _deserialiseArray(strOrBuffer) {
var arrItems = strOrBuffer.split(',');
return Uint8Array.from(arrItems);
};
var loadWalletData = function loadWalletData(safeApp, serialisedWallet) {
var deserialisedWallet, walletMd, coins;
return Promise.resolve().then(function () {
console.log("Reading the coin wallet info...");
deserialisedWallet = _deserialiseArray(serialisedWallet);
return safeApp.mutableData.fromSerial(deserialisedWallet);
}).then(function (_resp) {
walletMd = _resp;
return _readEncryptedEntry(walletMd, WALLET_ENTRY_KEY_COINS);
}).then(function (_resp) {
coins = _resp;
return JSON.parse(_fromArrayBuffer(coins));
});
};
var storeCoinsToWallet = function storeCoinsToWallet(safeApp, serialisedWallet, coins) {
var walletMd, encKey, currentCoins, mutations, encValue;
return Promise.resolve().then(function () {
console.log("Saving coins in the wallet on the network...");
return safeApp.mutableData.fromSerial(_deserialiseArray(serialisedWallet));
}).then(function (_resp) {
walletMd = _resp;
return walletMd.encryptKey(WALLET_ENTRY_KEY_COINS);
}).then(function (_resp) {
encKey = _resp;
return walletMd.get(encKey);
}).then(function (_resp) {
currentCoins = _resp;
return safeApp.mutableData.newMutation();
}).then(function (_resp) {
mutations = _resp;
return walletMd.encryptValue(JSON.stringify(coins));
}).then(function (_resp) {
encValue = _resp;
return mutations.update(encKey, encValue, currentCoins.version + 1);
}).then(function () {
return walletMd.applyEntriesMutation(mutations);
}).then(function () {});
};
// TX Inbox management functions
var _genKeyPair = function _genKeyPair(safeApp) {
var encKeyPair, rawPubEncKey, rawSecEncKey, rawKeyPair;
return Promise.resolve().then(function () {
return safeApp.crypto.generateEncKeyPair();
}).then(function (_resp) {
encKeyPair = _resp;
return encKeyPair.pubEncKey.getRaw();
}).then(function (_resp) {
rawPubEncKey = _resp;
return encKeyPair.secEncKey.getRaw();
}).then(function (_resp) {
rawSecEncKey = _resp;
rawKeyPair = {
pk: rawPubEncKey.buffer.toString('hex'),
sk: rawSecEncKey.buffer.toString('hex')
};
return rawKeyPair;
});
};
var createTxInbox = function createTxInbox(safeApp, pk) {
var _baseInbox, encKeys, baseInbox, xorName, inboxMd, permSet;
return Promise.resolve().then(function () {
console.log("Creating TX inbox...");
return _genKeyPair(safeApp);
}).then(function (_resp) {
encKeys = _resp;
baseInbox = (_baseInbox = {}, _defineProperty(_baseInbox, TX_INBOX_ENTRY_KEY_WALLET_PK, pk), _defineProperty(_baseInbox, TX_INBOX_ENTRY_KEY_ENC_PK, encKeys.pk), _baseInbox);
return _genXorName(safeApp, pk);
}).then(function (_resp) {
xorName = _resp;
return safeApp.mutableData.newPublic(xorName, TYPE_TAG_WALLET_TX_INBOX);
}).then(function (_resp) {
inboxMd = _resp;
return inboxMd.quickSetup(baseInbox, TX_INBOX_METADATA_NAME, TX_INBOX_METADATA_DESC);
}).then(function () {
permSet = [ACTION_INSERT];
return inboxMd.setUserPermissions(window.safe.CONSTANTS.USER_ANYONE, permSet, 1);
}).then(function () {
return encKeys;
});
};
var _encrypt = function _encrypt(safeApp, input, pk) {
var pubEncKey, encrypted;
return Promise.resolve().then(function () {
if (Array.isArray(input)) {
input = input.toString();
}
return safeApp.crypto.pubEncKeyFromRaw(Buffer.from(pk, 'hex'));
}).then(function (_resp) {
pubEncKey = _resp;
return pubEncKey.encryptSealed(input);
}).then(function (_resp) {
encrypted = _resp;
return encrypted;
});
};
var _decryptTxs = function _decryptTxs(safeApp, encryptedTxs, encPk, encSk) {
function _recursive2() {
if (i < encryptedTxs.length) {
return Promise.resolve().then(function () {
encTx = encryptedTxs[i];
rawPk = Buffer.from(encPk, 'hex');
rawSk = Buffer.from(encSk, 'hex');
return safeApp.crypto.generateEncKeyPairFromRaw(rawPk, rawSk);
}).then(function (_resp) {
encKeyPair = _resp;
return encKeyPair.decryptSealed(encTx.txInfo);
}).then(function (_resp) {
decrypted = _resp;
parsedTxInfo = JSON.parse(_fromArrayBuffer(decrypted));
decryptedTxs.push(_extends({ id: encTx.id, version: encTx.version }, parsedTxInfo));
i++;
return _recursive2();
});
}
}
var decryptedTxs, i, encTx, rawPk, rawSk, encKeyPair, decrypted, parsedTxInfo;
return Promise.resolve().then(function () {
decryptedTxs = [];
i = 0;
return _recursive2();
}).then(function () {
return decryptedTxs;
});
};
var readTxInboxData = function readTxInboxData(safeApp, pk, encPk, encSk) {
var encryptedTxs, xorName, inboxMd, entries, entriesList, decryptedTxs;
return Promise.resolve().then(function () {
encryptedTxs = [];
return _genXorName(safeApp, pk);
}).then(function (_resp) {
xorName = _resp;
return safeApp.mutableData.newPublic(xorName, TYPE_TAG_WALLET_TX_INBOX);
}).then(function (_resp) {
inboxMd = _resp;
return inboxMd.getEntries();
}).then(function (_resp) {
entries = _resp;
return entries.listEntries();
}).then(function (_resp) {
entriesList = _resp;
entriesList.forEach(function (entry) {
var id = _fromArrayBuffer(entry.key);
var txInfo = entry.value.buf;
// Ignore the Public encryption key entry, the metadata entry, and soft-deleted entries.
if (id !== TX_INBOX_ENTRY_KEY_WALLET_PK && id !== TX_INBOX_ENTRY_KEY_ENC_PK && id !== ENTRY_KEY_MD_METADATA && txInfo.length > 0) {
encryptedTxs.push({ id: id, txInfo: txInfo, version: entry.value.version });
}
});
return _decryptTxs(safeApp, encryptedTxs, encPk, encSk);
}).then(function (_resp) {
decryptedTxs = _resp;
return decryptedTxs;
});
};
var removeTxInboxData = function removeTxInboxData(safeApp, pk, txs) {
function _recursive3() {
if (i < txs.length) {
return Promise.resolve().then(function () {
tx = txs[i];
return mutations.delete(tx.id, tx.version + 1);
}).then(function () {
i++;
return _recursive3();
});
}
}
var mutations, i, tx, xorName, txInboxMd;
return Promise.resolve().then(function () {
return safeApp.mutableData.newMutation();
}).then(function (_resp) {
mutations = _resp;
i = 0;
return _recursive3();
}).then(function () {
return _genXorName(safeApp, pk);
}).then(function (_resp) {
xorName = _resp;
return safeApp.mutableData.newPublic(xorName, TYPE_TAG_WALLET_TX_INBOX);
}).then(function (_resp) {
txInboxMd = _resp;
return txInboxMd.applyEntriesMutation(mutations);
}).then(function () {});
};
var _fetchTxInboxFromWebId = function _fetchTxInboxFromWebId(safeApp, webId) {
var _ref, webIdMd, resourceType, webIdRdf, baseUri, walletGraph, SAFETERMS, xornameMatch, typetagMatch, txInboxMd, xorName, typeTag, WALLETTERMS, hasMeAlready, webIdWithHashTag, walletInboxMatch, txInboxXorUrl, _ref2, content;
return Promise.resolve().then(function () {
return safeApp.fetch(webId);
}).then(function (_resp) {
_ref = _resp;
webIdMd = _ref.content;
resourceType = _ref.resourceType;
if (resourceType !== 'RDF') {
throw 'Service is not mapped to a WebID RDF';
}webIdRdf = webIdMd.emulateAs('rdf');
return webIdRdf.nowOrWhenFetched();
}).then(function () {
baseUri = webId.split('#')[0];
// first try to find old format 'walletInbox' graph
walletGraph = webIdRdf.sym(baseUri + '/walletInbox');
SAFETERMS = webIdRdf.namespace('http://safenetwork.org/safevocab/');
xornameMatch = webIdRdf.statementsMatching(walletGraph, SAFETERMS('xorName'), undefined);
typetagMatch = webIdRdf.statementsMatching(walletGraph, SAFETERMS('typeTag'), undefined);
txInboxMd = void 0;
if (xornameMatch[0] && typetagMatch[0]) {
return Promise.resolve().then(function () {
xorName = xornameMatch[0].object.value.split(',');
typeTag = parseInt(typetagMatch[0].object.value);
return safeApp.mutableData.newPublic(xorName, typeTag);
}).then(function (_resp) {
txInboxMd = _resp;
});
} else {
return Promise.resolve().then(function () {
// let's try to find the new format for wallet inbox XOR-URL
WALLETTERMS = webIdRdf.namespace('https://w3id.org/cc#');
hasMeAlready = webId.includes('#me');
// TODO: we should actually be checking which is the default agent in the WebID
webIdWithHashTag = hasMeAlready ? webIdRdf.sym(webId) : webIdRdf.sym(webId + '#me');
walletInboxMatch = webIdRdf.statementsMatching(webIdWithHashTag, WALLETTERMS('inbox'), undefined);
if (!walletInboxMatch[0]) {
throw Error('No wallet TX inbox link found in WebID or it lacks information');
}
txInboxXorUrl = walletInboxMatch[0].object.value;
return safeApp.fetch(txInboxXorUrl);
}).then(function (_resp) {
_ref2 = _resp;
content = _ref2.content;
txInboxMd = content;
});
}
}).then(function () {
return txInboxMd;
});
};
var _fetchTxInbox = function _fetchTxInbox(safeApp, recipient) {
var txInboxMd, xorName, typeTag;
return Promise.resolve().then(function () {
txInboxMd = void 0;
if (recipient.toLowerCase().startsWith('safe://')) {
return Promise.resolve().then(function () {
// the recipient is a WebID, let's resolve the linked wallet TX inbox
console.log('Fetching WebID:', recipient);
return _fetchTxInboxFromWebId(safeApp, recipient);
}).then(function (_resp) {
txInboxMd = _resp;
});
} else {
return Promise.resolve().then(function () {
return _genXorName(safeApp, recipient);
}).then(function (_resp) {
// recipient is just a PK
xorName = _resp;
typeTag = TYPE_TAG_WALLET_TX_INBOX;
return safeApp.mutableData.newPublic(xorName, typeTag);
}).then(function (_resp) {
txInboxMd = _resp;
});
}
}).then(function () {
return txInboxMd;
});
};
var sendTxNotif = function sendTxNotif(safeApp, recipient, coinIds, msg) {
var txId, tx, txNotif, txInboxMd, encPk, encryptedTx, mutations;
return Promise.resolve().then(function () {
txId = crypto.randomBytes(16).toString('hex');
tx = {
coinIds: coinIds,
msg: msg,
date: new Date().toUTCString()
};
txNotif = JSON.stringify(tx);
// we expect the recipient to be a pk but it will also work if it's a WebID
console.log("Sending TX notification to recipient. TX id: ", txId);
return _fetchTxInbox(safeApp, recipient);
}).then(function (_resp) {
txInboxMd = _resp;
return txInboxMd.get(TX_INBOX_ENTRY_KEY_ENC_PK);
}).then(function (_resp) {
encPk = _resp;
return _encrypt(safeApp, txNotif, encPk.buf.toString());
}).then(function (_resp) {
encryptedTx = _resp;
return safeApp.mutableData.newMutation();
}).then(function (_resp) {
mutations = _resp;
return mutations.insert(txId, encryptedTx);
}).then(function () {
return txInboxMd.applyEntriesMutation(mutations);
}).then(function () {});
};
var _checkOwnership = function _checkOwnership(coin, pk) {
var coinData = JSON.parse(coin);
console.log("Coin data: ", coinData);
if (coinData.owner !== pk) {
throw Error("Ownership doesn't match", pk, coinData);
}
return coinData;
};
var _fetchCoin = function _fetchCoin(safeApp, coinId) {
var coinMd, coin;
return Promise.resolve().then(function () {
return safeApp.mutableData.newPublic(Buffer.from(coinId, 'hex'), TYPE_TAG_THANKS_COIN);
}).then(function (_resp) {
coinMd = _resp;
return coinMd.get(COIN_ENTRY_KEY_DATA);
}).then(function (_resp) {
coin = _resp;
return { coin: coin, coinMd: coinMd };
});
};
var checkOwnership = function checkOwnership(safeApp, coinId, pk) {
var _ref3, coin, coinMd;
return Promise.resolve().then(function () {
console.log("Reading coin data...", pk, coinId);
return _fetchCoin(safeApp, coinId);
}).then(function (_resp) {
_ref3 = _resp;
coin = _ref3.coin;
coinMd = _ref3.coinMd;
return _checkOwnership(coin.buf.toString(), pk);
});
};
var transferCoin = function transferCoin(safeApp, coinId, pk, sk, recipient) {
var _ref4, coin, coinMd, coinData, txInboxMd, recipientPk, recipientPkStr, mutations;
return Promise.resolve().then(function () {
console.log("Transfering coin's ownership in the network...", coinId, recipient);
return _fetchCoin(safeApp, coinId);
}).then(function (_resp) {
_ref4 = _resp;
coin = _ref4.coin;
coinMd = _ref4.coinMd;
coinData = _checkOwnership(coin.buf.toString(), pk);
return _fetchTxInbox(safeApp, recipient);
}).then(function (_resp) {
txInboxMd = _resp;
return txInboxMd.get(TX_INBOX_ENTRY_KEY_WALLET_PK);
}).then(function (_resp) {
recipientPk = _resp;
recipientPkStr = recipientPk.buf.toString();
coinData.owner = recipientPkStr;
coinData.prev_owner = pk;
console.log("Coin's new ownership: ", coinData);
return safeApp.mutableData.newMutation();
}).then(function (_resp) {
mutations = _resp;
return mutations.update(COIN_ENTRY_KEY_DATA, JSON.stringify(coinData), coin.version + 1);
}).then(function () {
return coinMd.applyEntriesMutation(mutations);
}).then(function () {
return recipientPkStr;
});
};
var updateLinkInWebId = exports.updateLinkInWebId = function updateLinkInWebId(safeApp, webIdUrl, txInboxPk) {
var _ref5, webIdMd, resourceType, webIdRdf, baseUri, WALLETTERMS, hasMeAlready, webIdWithHashTag, xorName, inboxMd, _ref6, xorUrl, nameAndTag, authReqUri, authUri;
return Promise.resolve().then(function () {
console.log("Updating link in WebID:", webIdUrl, txInboxPk);
return safeApp.fetch(webIdUrl);
}).then(function (_resp) {
_ref5 = _resp;
webIdMd = _ref5.content;
resourceType = _ref5.resourceType;
if (resourceType !== 'RDF') {
throw 'Service is not mapped to a WebID RDF';
}webIdRdf = webIdMd.emulateAs('rdf');
return webIdRdf.nowOrWhenFetched();
}).then(function () {
baseUri = webIdUrl.split('#')[0];
// remove old format 'walletInbox' graph
webIdRdf.removeMany(webIdRdf.sym(baseUri + '/walletInbox'), undefined, undefined);
WALLETTERMS = webIdRdf.namespace('https://w3id.org/cc#');
hasMeAlready = webIdUrl.includes('#me');
// TODO: we should actually be checking which is the default agent in the WebID
webIdWithHashTag = hasMeAlready ? webIdRdf.sym(webIdUrl) : webIdRdf.sym(webIdUrl + '#me');
webIdRdf.removeMany(webIdWithHashTag, WALLETTERMS('inbox'), undefined);
if (txInboxPk) {
return Promise.resolve().then(function () {
return _genXorName(safeApp, txInboxPk);
}).then(function (_resp) {
xorName = _resp;
return safeApp.mutableData.newPublic(xorName, TYPE_TAG_WALLET_TX_INBOX);
}).then(function (_resp) {
inboxMd = _resp;
return inboxMd.getNameAndTag();
}).then(function (_resp) {
_ref6 = _resp;
xorUrl = _ref6.xorUrl;
webIdRdf.add(webIdWithHashTag, WALLETTERMS('inbox'), webIdRdf.sym(xorUrl));
});
}
}).then(function () {
return Promise.resolve().then(function () {
return webIdRdf.commit();
}).catch(function (error) {
return Promise.resolve().then(function () {
return webIdMd.getNameAndTag();
}).then(function (_resp) {
nameAndTag = _resp;
console.log("Authorising application to update WebID...");
return safeApp.auth.genShareMDataUri([{
typeTag: nameAndTag.typeTag,
name: nameAndTag.name,
perms: ['Insert', 'Update', 'Delete']
}]);
}).then(function (_resp) {
authReqUri = _resp;
return window.safe.authorise(authReqUri);
}).then(function (_resp) {
authUri = _resp;
return safeApp.auth.loginFromUri(authUri);
}).then(function () {
return safeApp.auth.refreshContainersPermissions();
}).then(function () {
console.log("Committing update in WebID now...");
return webIdRdf.commit();
});
});
}).then(function () {
console.log("WebID updated with new link to TX inbox");
});
};
module.exports = {
createWallet: createWallet,
loadWalletData: loadWalletData,
storeCoinsToWallet: storeCoinsToWallet,
createTxInbox: createTxInbox,
readTxInboxData: readTxInboxData,
removeTxInboxData: removeTxInboxData,
sendTxNotif: sendTxNotif,
checkOwnership: checkOwnership,
transferCoin: transferCoin,
updateLinkInWebId: updateLinkInWebId
};