checkpoint

This commit is contained in:
howard-stearns 2017-08-18 10:03:00 -07:00
parent 38e5c4fde0
commit 3d401129fb
10 changed files with 127 additions and 83 deletions

View file

@ -73,7 +73,7 @@ Item {
console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message); console.log("Web Entity JS message: " + sourceID + " " + lineNumber + " " + message);
}); });
root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface)"; root.profile.httpUserAgent = "Mozilla/5.0 Chrome (HighFidelityInterface WithHFC)";
} }
onFeaturePermissionRequested: { onFeaturePermissionRequested: {

View file

@ -35,8 +35,8 @@ Rectangle {
Hifi.QmlCommerce { Hifi.QmlCommerce {
id: commerce; id: commerce;
onBuyResult: { onBuyResult: {
if (failureMessage.length) { if (result.status !== 'success') {
buyButton.text = "Buy Failed"; buyButton.text = result.message;
buyButton.enabled = false; buyButton.enabled = false;
} else { } else {
if (urlHandler.canHandleUrl(itemHref)) { if (urlHandler.canHandleUrl(itemHref)) {
@ -46,20 +46,21 @@ Rectangle {
} }
} }
onBalanceResult: { onBalanceResult: {
if (failureMessage.length) { if (result.status !== 'success') {
console.log("Failed to get balance", failureMessage); console.log("Failed to get balance", result.message);
} else { } else {
balanceReceived = true; balanceReceived = true;
hfcBalanceText.text = balance; hfcBalanceText.text = result.data.balance;
balanceAfterPurchase = balance - parseInt(itemPriceText.text, 10); balanceAfterPurchase = result.data.balance - parseInt(itemPriceText.text, 10);
} }
} }
onInventoryResult: { onInventoryResult: {
if (failureMessage.length) { if (result.status !== 'success') {
console.log("Failed to get inventory", failureMessage); console.log("Failed to get inventory", result.message);
} else { } else {
inventoryReceived = true; inventoryReceived = true;
if (inventoryContains(inventory.assets, itemId)) { console.log('inventory fixme', JSON.stringify(result));
if (inventoryContains(result.data.assets, itemId)) {
alreadyOwned = true; alreadyOwned = true;
} else { } else {
alreadyOwned = false; alreadyOwned = false;

View file

@ -30,17 +30,17 @@ Rectangle {
Hifi.QmlCommerce { Hifi.QmlCommerce {
id: commerce; id: commerce;
onBalanceResult: { onBalanceResult: {
if (failureMessage.length) { if (result.status !== 'success') {
console.log("Failed to get balance", failureMessage); console.log("Failed to get balance", result.message);
} else { } else {
hfcBalanceText.text = balance; hfcBalanceText.text = result.data.balance;
} }
} }
onInventoryResult: { onInventoryResult: {
if (failureMessage.length) { if (result.status !== 'success') {
console.log("Failed to get inventory", failureMessage); console.log("Failed to get inventory", result.message);
} else { } else {
inventoryContentsList.model = inventory.assets; inventoryContentsList.model = result.data.assets;
} }
} }
onSecurityImageResult: { onSecurityImageResult: {

View file

@ -10,72 +10,100 @@
// //
#include <QJsonObject> #include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include "AccountManager.h" #include "AccountManager.h"
#include "Wallet.h" #include "Wallet.h"
#include "Ledger.h" #include "Ledger.h"
#include "CommerceLogging.h" #include "CommerceLogging.h"
// inventory answers {status: 'success', data: {assets: [{id: "guid", title: "name", preview: "url"}....]}}
// balance answers {status: 'success', data: {balance: integer}}
// buy and receive_at answer {status: 'success'}
QJsonObject Ledger::apiResponse(const QString& label, QNetworkReply& reply) {
QByteArray response = reply.readAll();
QJsonObject data = QJsonDocument::fromJson(response).object();
qInfo(commerce) << label << "response" << QJsonDocument(data).toJson(QJsonDocument::Compact);
return data;
}
// Non-200 responses are not json:
QJsonObject Ledger::failResponse(const QString& label, QNetworkReply& reply) {
QString response = reply.readAll();
qWarning(commerce) << "FAILED" << label << response;
QJsonObject result
{
{ "status", "fail" },
{ "message", response }
};
return result;
}
#define ApiHandler(NAME) void Ledger::NAME##Success(QNetworkReply& reply) { emit NAME##Result(apiResponse(#NAME, reply)); }
#define FailHandler(NAME) void Ledger::NAME##Failure(QNetworkReply& reply) { emit NAME##Result(failResponse(#NAME, reply)); }
#define Handler(NAME) ApiHandler(NAME) FailHandler(NAME)
Handler(buy)
Handler(receiveAt)
Handler(balance)
Handler(inventory)
void Ledger::send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, QJsonObject request) {
auto accountManager = DependencyManager::get<AccountManager>();
const QString URL = "/api/v1/commerce/";
JSONCallbackParameters callbackParams(this, success, this, fail);
qCInfo(commerce) << "Sending" << endpoint << QJsonDocument(request).toJson(QJsonDocument::Compact);
accountManager->sendRequest(URL + endpoint,
AccountManagerAuth::Required,
method,
callbackParams,
QJsonDocument(request).toJson());
}
void Ledger::signedSend(const QString& propertyName, const QByteArray& text, const QString& key, const QString& endpoint, const QString& success, const QString& fail) {
auto wallet = DependencyManager::get<Wallet>();
QString signature = key.isEmpty() ? "" : wallet->signWithKey(text, key);
QJsonObject request;
request[propertyName] = QString(text);
request["signature"] = signature;
send(endpoint, success, fail, QNetworkAccessManager::PutOperation, request);
}
void Ledger::keysQuery(const QString& endpoint, const QString& success, const QString& fail) {
auto wallet = DependencyManager::get<Wallet>();
QJsonObject request;
request["public_keys"] = QJsonArray::fromStringList(wallet->listPublicKeys());
send(endpoint, success, fail, QNetworkAccessManager::PostOperation, request);
}
void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername) { void Ledger::buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername) {
QJsonObject transaction; QJsonObject transaction;
transaction["hfc_key"] = hfc_key; transaction["hfc_key"] = hfc_key;
transaction["hfc"] = cost; transaction["cost"] = cost;
transaction["asset_id"] = asset_id; transaction["asset_id"] = asset_id;
transaction["inventory_key"] = inventory_key; transaction["inventory_key"] = inventory_key;
transaction["inventory_buyer_username"] = buyerUsername; transaction["inventory_buyer_username"] = buyerUsername;
QJsonDocument transactionDoc{ transaction }; QJsonDocument transactionDoc{ transaction };
auto transactionString = transactionDoc.toJson(QJsonDocument::Compact); auto transactionString = transactionDoc.toJson(QJsonDocument::Compact);
signedSend("transaction", transactionString, hfc_key, "buy", "buySuccess", "buyFailure");
auto wallet = DependencyManager::get<Wallet>();
QString signature = wallet->signWithKey(transactionString, hfc_key);
QJsonObject request;
request["transaction"] = QString(transactionString);
request["signature"] = signature;
qCInfo(commerce) << "Transaction:" << QJsonDocument(request).toJson(QJsonDocument::Compact);
// FIXME: talk to server instead
if (_inventory.contains(asset_id)) {
// This is here more for testing than as a definition of semantics.
// When we have popcerts, you will certainly be able to buy a new instance of an item that you already own a different instance of.
// I'm not sure what the server should do for now in this project's MVP.
return emit buyResult("Already owned.");
}
if (initializedBalance() < cost) {
return emit buyResult("Insufficient funds.");
}
_balance -= cost;
QJsonObject inventoryAdditionObject;
inventoryAdditionObject["id"] = asset_id;
inventoryAdditionObject["title"] = "Test Title";
inventoryAdditionObject["preview"] = "https://www.aspca.org/sites/default/files/cat-care_cat-nutrition-tips_overweight_body4_left.jpg";
_inventory.push_back(inventoryAdditionObject);
emit buyResult("");
} }
bool Ledger::receiveAt(const QString& hfc_key) { bool Ledger::receiveAt(const QString& hfc_key, const QString& old_key) {
auto accountManager = DependencyManager::get<AccountManager>(); auto accountManager = DependencyManager::get<AccountManager>();
if (!accountManager->isLoggedIn()) { if (!accountManager->isLoggedIn()) {
qCWarning(commerce) << "Cannot set receiveAt when not logged in."; qCWarning(commerce) << "Cannot set receiveAt when not logged in.";
emit receiveAtResult("Not logged in"); QJsonObject result{ { "status", "fail" }, { "message", "Not logged in" } };
emit receiveAtResult(result);
return false; // We know right away that we will fail, so tell the caller. return false; // We know right away that we will fail, so tell the caller.
} }
auto username = accountManager->getAccountInfo().getUsername();
qCInfo(commerce) << "Setting default receiving key for" << username; signedSend("public_key", hfc_key.toUtf8(), old_key, "receive_at", "receiveAtSuccess", "receiveAtFailure");
emit receiveAtResult(""); // FIXME: talk to server instead.
return true; // Note that there may still be an asynchronous signal of failure that callers might be interested in. return true; // Note that there may still be an asynchronous signal of failure that callers might be interested in.
} }
void Ledger::balance(const QStringList& keys) { void Ledger::balance(const QStringList& keys) {
// FIXME: talk to server instead keysQuery("balance", "balanceSuccess", "balanceFailure");
qCInfo(commerce) << "Balance:" << initializedBalance();
emit balanceResult(_balance, "");
} }
void Ledger::inventory(const QStringList& keys) { void Ledger::inventory(const QStringList& keys) {
// FIXME: talk to server instead keysQuery("inventory", "inventorySuccess", "inventoryFailure");
QJsonObject inventoryObject;
inventoryObject.insert("success", true);
inventoryObject.insert("assets", _inventory);
qCInfo(commerce) << "Inventory:" << inventoryObject;
emit inventoryResult(inventoryObject, "");
} }

View file

@ -14,9 +14,10 @@
#ifndef hifi_Ledger_h #ifndef hifi_Ledger_h
#define hifi_Ledger_h #define hifi_Ledger_h
#include <QJsonObject>
#include <DependencyManager.h> #include <DependencyManager.h>
#include <qjsonobject.h> #include <QtNetwork/QNetworkReply>
#include <qjsonarray.h>
class Ledger : public QObject, public Dependency { class Ledger : public QObject, public Dependency {
Q_OBJECT Q_OBJECT
@ -24,21 +25,32 @@ class Ledger : public QObject, public Dependency {
public: public:
void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername = ""); void buy(const QString& hfc_key, int cost, const QString& asset_id, const QString& inventory_key, const QString& buyerUsername = "");
bool receiveAt(const QString& hfc_key); bool receiveAt(const QString& hfc_key, const QString& old_key);
void balance(const QStringList& keys); void balance(const QStringList& keys);
void inventory(const QStringList& keys); void inventory(const QStringList& keys);
signals: signals:
void buyResult(const QString& failureReason); void buyResult(QJsonObject result);
void receiveAtResult(const QString& failureReason); void receiveAtResult(QJsonObject result);
void balanceResult(int balance, const QString& failureReason); void balanceResult(QJsonObject result);
void inventoryResult(QJsonObject inventory, const QString& failureReason); void inventoryResult(QJsonObject result);
public slots:
void buySuccess(QNetworkReply& reply);
void buyFailure(QNetworkReply& reply);
void receiveAtSuccess(QNetworkReply& reply);
void receiveAtFailure(QNetworkReply& reply);
void balanceSuccess(QNetworkReply& reply);
void balanceFailure(QNetworkReply& reply);
void inventorySuccess(QNetworkReply& reply);
void inventoryFailure(QNetworkReply& reply);
private: private:
// These in-memory caches is temporary, until we start sending things to the server. QJsonObject apiResponse(const QString& label, QNetworkReply& reply);
int _balance{ -1 }; QJsonObject failResponse(const QString& label, QNetworkReply& reply);
QJsonArray _inventory{}; void send(const QString& endpoint, const QString& success, const QString& fail, QNetworkAccessManager::Operation method, QJsonObject request);
int initializedBalance() { if (_balance < 0) _balance = 100; return _balance; } void keysQuery(const QString& endpoint, const QString& success, const QString& fail);
void signedSend(const QString& propertyName, const QByteArray& text, const QString& key, const QString& endpoint, const QString& success, const QString& fail);
}; };
#endif // hifi_Ledger_h #endif // hifi_Ledger_h

View file

@ -31,14 +31,15 @@ void QmlCommerce::buy(const QString& assetId, int cost, const QString& buyerUser
auto wallet = DependencyManager::get<Wallet>(); auto wallet = DependencyManager::get<Wallet>();
QStringList keys = wallet->listPublicKeys(); QStringList keys = wallet->listPublicKeys();
if (keys.count() == 0) { if (keys.count() == 0) {
return emit buyResult("Uninitialized Wallet."); QJsonObject result{ { "status", "fail" }, { "message", "Uninitialized Wallet." } };
return emit buyResult(result);
} }
QString key = keys[0]; QString key = keys[0];
// For now, we receive at the same key that pays for it. // For now, we receive at the same key that pays for it.
ledger->buy(key, cost, assetId, key, buyerUsername); ledger->buy(key, cost, assetId, key, buyerUsername);
// FIXME: until we start talking to server, report post-transaction balance and inventory so we can see log for testing. // FIXME: until we start talking to server, report post-transaction balance and inventory so we can see log for testing.
balance(); /*balance();
inventory(); inventory();*/
} }
void QmlCommerce::balance() { void QmlCommerce::balance() {

View file

@ -15,6 +15,7 @@
#ifndef hifi_QmlCommerce_h #ifndef hifi_QmlCommerce_h
#define hifi_QmlCommerce_h #define hifi_QmlCommerce_h
#include <QJsonObject>
#include <OffscreenQmlDialog.h> #include <OffscreenQmlDialog.h>
class QmlCommerce : public OffscreenQmlDialog { class QmlCommerce : public OffscreenQmlDialog {
@ -25,11 +26,11 @@ public:
QmlCommerce(QQuickItem* parent = nullptr); QmlCommerce(QQuickItem* parent = nullptr);
signals: signals:
void buyResult(const QString& failureMessage); void buyResult(QJsonObject result);
// Balance and Inventory are NOT properties, because QML can't change them (without risk of failure), and // Balance and Inventory are NOT properties, because QML can't change them (without risk of failure), and
// because we can't scalably know of out-of-band changes (e.g., another machine interacting with the block chain). // because we can't scalably know of out-of-band changes (e.g., another machine interacting with the block chain).
void balanceResult(int balance, const QString& failureMessage); void balanceResult(QJsonObject result);
void inventoryResult(QJsonObject inventory, const QString& failureMessage); void inventoryResult(QJsonObject result);
void securityImageResult(uint imageID); void securityImageResult(uint imageID);
protected: protected:

View file

@ -205,7 +205,7 @@ bool Wallet::createIfNeeded() {
qCDebug(commerce) << "read private key"; qCDebug(commerce) << "read private key";
RSA_free(key); RSA_free(key);
// K -- add the public key since we have a legit private key associated with it // K -- add the public key since we have a legit private key associated with it
_publicKeys.push_back(QUrl::toPercentEncoding(publicKey.toBase64())); _publicKeys.push_back(publicKey.toBase64());
return false; return false;
} }
} }
@ -216,16 +216,17 @@ bool Wallet::createIfNeeded() {
bool Wallet::generateKeyPair() { bool Wallet::generateKeyPair() {
qCInfo(commerce) << "Generating keypair."; qCInfo(commerce) << "Generating keypair.";
auto keyPair = generateRSAKeypair(); auto keyPair = generateRSAKeypair();
QString oldKey = _publicKeys.count() == 0 ? "" : _publicKeys.last();
_publicKeys.push_back(QUrl::toPercentEncoding(keyPair.first->toBase64())); QString key = keyPair.first->toBase64();
qCDebug(commerce) << "public key:" << _publicKeys.last(); _publicKeys.push_back(key);
qCDebug(commerce) << "public key:" << key;
// It's arguable whether we want to change the receiveAt every time, but: // It's arguable whether we want to change the receiveAt every time, but:
// 1. It's certainly needed the first time, when createIfNeeded answers true. // 1. It's certainly needed the first time, when createIfNeeded answers true.
// 2. It is maximally private, and we can step back from that later if desired. // 2. It is maximally private, and we can step back from that later if desired.
// 3. It maximally exercises all the machinery, so we are most likely to surface issues now. // 3. It maximally exercises all the machinery, so we are most likely to surface issues now.
auto ledger = DependencyManager::get<Ledger>(); auto ledger = DependencyManager::get<Ledger>();
return ledger->receiveAt(_publicKeys.last()); return ledger->receiveAt(key, oldKey);
} }
QStringList Wallet::listPublicKeys() { QStringList Wallet::listPublicKeys() {
qCInfo(commerce) << "Enumerating public keys."; qCInfo(commerce) << "Enumerating public keys.";
@ -260,7 +261,7 @@ QString Wallet::signWithKey(const QByteArray& text, const QString& key) {
RSA_free(rsaPrivateKey); RSA_free(rsaPrivateKey);
if (encryptReturn != -1) { if (encryptReturn != -1) {
return QUrl::toPercentEncoding(signature.toBase64()); return signature.toBase64();
} }
} }
return QString(); return QString();

View file

@ -36,7 +36,7 @@ void HFTabletWebEngineRequestInterceptor::interceptRequest(QWebEngineUrlRequestI
} }
static const QString USER_AGENT = "User-Agent"; static const QString USER_AGENT = "User-Agent";
QString tokenString = "Chrome/48.0 (HighFidelityInterface)"; QString tokenString = "Chrome/48.0 (HighFidelityInterface WithHFC)";
info.setHttpHeader(USER_AGENT.toLocal8Bit(), tokenString.toLocal8Bit()); info.setHttpHeader(USER_AGENT.toLocal8Bit(), tokenString.toLocal8Bit());
} else { } else {
static const QString USER_AGENT = "User-Agent"; static const QString USER_AGENT = "User-Agent";

View file

@ -114,7 +114,7 @@
itemId: id, itemId: id,
itemName: name, itemName: name,
itemAuthor: author, itemAuthor: author,
itemPrice: Math.round(Math.random() * 50), itemPrice: price ? parseInt(price, 10) : Math.round(Math.random() * 50),
itemHref: href itemHref: href
})); }));
} }
@ -129,7 +129,7 @@
buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'), buyButtonClicked($(this).closest('.grid-item').attr('data-item-id'),
$(this).closest('.grid-item').find('.item-title').text(), $(this).closest('.grid-item').find('.item-title').text(),
$(this).closest('.grid-item').find('.creator').find('.value').text(), $(this).closest('.grid-item').find('.creator').find('.value').text(),
10, $(this).closest('.grid-item').find('.item-cost').text(),
$(this).attr('data-href')); $(this).attr('data-href'));
}); });
} }
@ -165,7 +165,7 @@
buyButtonClicked(window.location.pathname.split("/")[3], buyButtonClicked(window.location.pathname.split("/")[3],
$('#top-center').find('h1').text(), $('#top-center').find('h1').text(),
$('#creator').find('.value').text(), $('#creator').find('.value').text(),
10, $('.item-cost').text(),
href); href);
}); });
addInventoryButton(); addInventoryButton();