Skip to content

Commit

Permalink
Add periodic tracking and periodic validation for payment pointers.
Browse files Browse the repository at this point in the history
  • Loading branch information
vezwork committed Nov 20, 2020
1 parent c03f952 commit 12545f5
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 45 deletions.
167 changes: 125 additions & 42 deletions src/content_main.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
const NEW_PAYMENT_POINTER_CHECK_RATE_MS = 1000; // 1 second
const PAYMENT_POINTER_VALIDATION_RATE_MS = 1000 * 60 * 60; // 1 hour

/**
* Content scripts can only see a "clean version" of the DOM, i.e. a version of the DOM without
* properties which are added by JavaScript, such as document.monetization!
Expand Down Expand Up @@ -32,17 +35,6 @@ main();
* Main function to initiate the application.
*/
async function main() {
// TODO: check payment pointer periodically for existence and validity
const {
isValid,
paymentPointer
} = await getAndValidatePaymentPointer();

setExtensionIconMonetizationState(isValid);

// paymentPointer will be null if it doesn't exist or is invalid
await storeDataIntoAkitaFormat({ paymentPointer: paymentPointer }, AKITA_DATA_TYPE.PAYMENT);

// Test storing assets
// await storeDataIntoAkitaFormat({
// paymentPointer: paymentPointer,
Expand All @@ -58,6 +50,8 @@ async function main() {
// storeDataIntoAkitaFormat(null, AKITA_DATA_TYPE.PAYMENT);
//});

trackPaymentPointer();
handleUpdatingExtensionIconOnTabVisibilityChange();
await trackTimeOnSite();
await trackVisitToSite();

Expand All @@ -77,8 +71,11 @@ async function main() {
* If true, a pink $ badge is displayed. If false, just the dog face without the tongue is used as the icon.
*/
function setExtensionIconMonetizationState(isCurrentlyMonetized) {
const webBrowser = chrome ? chrome : browser;
webBrowser.runtime.sendMessage({ isCurrentlyMonetized });
const tabIsVisible = !document.hidden;
if (tabIsVisible) {
const webBrowser = chrome ? chrome : browser;
webBrowser.runtime.sendMessage({ isCurrentlyMonetized });
}
}

/***********************************************************
Expand Down Expand Up @@ -114,12 +111,6 @@ async function trackTimeOnSite() {
} else {
// The page is now visible
docVisibleTime = getCurrentTime();

const {
isValid,
paymentPointer
} = await getAndValidatePaymentPointer();
setExtensionIconMonetizationState(isValid);
}
});

Expand Down Expand Up @@ -171,42 +162,134 @@ async function storeRecentTimeSpent(recentTimeSpent) {
}

/***********************************************************
* Validate Payment Pointer
* Track and Validate Payment Pointer
***********************************************************/

/**
* Check for a monetization meta tag on the website and verify that
* the payment pointer is valid (resolves to a valid SPSP endpoint).
*
* TODO: use enum to indicate no meta tag, meta tag + valid endpoint,
* meta tag + invalid endpoint.
*
* @return {Promise<{ isPaymentPointerValid: boolean, paymentPointer:string}>}
* isPaymentPointerValid is true if both monetization is present and the payment endpoint is valid.
* paymentPointer is the paymentPointer if it is found in the monetization meta tag, otherwise null.
* getPaymentPointerFromPage gets the payment pointer as a string from the monetization meta tag
* on the current page. If there is no monetization meta tag on the page, null is returned.
* For more info on the monetization meta tag:
* - https://webmonetization.org/docs/getting-started
* - https://webmonetization.org/specification.html#meta-tags-set
*
* @return {string|null} The payment pointer on the current page, or null if no meta tag is present.
*/
async function getAndValidatePaymentPointer() {
const monetizationMeta = document.querySelector('meta[name="monetization"]');
let paymentPointer = (monetizationMeta) ? monetizationMeta.content : null;
let isValid = false;
function getPaymentPointerFromPage() {
const monetizationMetaTag = document.querySelector('meta[name="monetization"]');

if (null === monetizationMeta) {
/* No monetization meta tag provided */
if (monetizationMetaTag) {
return monetizationMetaTag.content;
} else {
return null;
}
}

/**
* Checks every NEW_PAYMENT_POINTER_CHECK_RATE_MS milliseconds
* to see if a new payment pointer is added to the page. Calls `handleValidationAndSetExtensionIcon`
* to set the extension icon.
*/
function trackPaymentPointer() {
let cachedPaymentPointer = null;

setInterval(async () => {
const paymentPointerOnPage = getPaymentPointerFromPage();

const isNewPaymentPointer =
(paymentPointerOnPage !== null) &&
(paymentPointerOnPage !== cachedPaymentPointer);

if (isNewPaymentPointer) {
// Note that paymentPointerOnPage may be newly added to the current page and tab,
// but it is possible that this payment pointer was recently validated in another tab.
// Therefore it is necessary to use handleValidationAndSetExtensionIcon which checks the
// browser storage to see if the payment pointer has been recently validated.
handleValidationAndSetExtensionIcon(paymentPointerOnPage);
cachedPaymentPointer = paymentPointerOnPage;
}
}, NEW_PAYMENT_POINTER_CHECK_RATE_MS);
}

/**
* Ensures that the extension icon is updated to the correct state when switching between tabs.
*/
function handleUpdatingExtensionIconOnTabVisibilityChange() {
document.addEventListener('visibilitychange', (event) => {
const tabIsVisible = !document.hidden;

if (tabIsVisible) {
const paymentPointerOnPage = getPaymentPointerFromPage();
if (paymentPointerOnPage === null) {
setExtensionIconMonetizationState(false);
} else {
handleValidationAndSetExtensionIcon(paymentPointerOnPage);
}
}
});
}

/**
* Given a payment pointer, if it is valid, this function sets the extension icon to its
* monetized state and stores the payment pointer in browser storage,
* otherwise the extension icon is set to its unmonetized state.
*
* @param {string} paymentPointer the payment pointer to validate.
* @returns {Promise<>} a promise that resolves when validation is finished.
*/
async function handleValidationAndSetExtensionIcon(paymentPointer) {
if (await isPaymentPointerRecentlyValidated(paymentPointer)) {
setExtensionIconMonetizationState(true);
} else {
// validate if not validated recently
if (await isPaymentPointerValid(paymentPointer)) {
isValid = true;
// Store the payment pointer and the UTC timestamp for the time it was validated (now).
await storeDataIntoAkitaFormat({
paymentPointer: paymentPointer,
validationTimestamp: Date.now()
}, AKITA_DATA_TYPE.PAYMENT);
setExtensionIconMonetizationState(true);
} else {
setExtensionIconMonetizationState(false);
}
}
}

return {
isValid,
paymentPointer
};
/**
* Checks the browser storage to see if Akita has validated the given payment pointer and stored a
* validation timestamp for it recently.
*
* @param {string} paymentPointer the payment pointer to check.
* @returns {Promise<boolean>} resolves to true if there is a timestamp retrieved from storage and
* the timestamp is recent (less than PAYMENT_POINTER_VALIDATION_RATE_MS from present time).
* Resolves false otherwise.
*/
async function isPaymentPointerRecentlyValidated(paymentPointer) {
const originData = await loadOriginData(window.location.origin);
let validationTimestamp = null;
if (originData && originData.paymentPointerMap[paymentPointer]) {
// get validationTimestamp from originData under the given payment pointer.
validationTimestamp =
originData.paymentPointerMap[paymentPointer].validationTimestamp;
}
if (validationTimestamp === null) {
return false;
}

// check if validationTimestamp is recent.
const timeSinceLastValidated = Date.now() - validationTimestamp;
if (timeSinceLastValidated < PAYMENT_POINTER_VALIDATION_RATE_MS) {
return true;
} else {
return false;
}
}

/**
* Check if a payment pointer is valid or not.
*
* Check if a payment pointer is valid (resolves to a valid SPSP endpoint).
* For more information on payment pointer resolution and validation see:
* - https://paymentpointers.org/syntax-resolution/#requirements
* - https://interledger.org/rfcs/0009-simple-payment-setup-protocol
*
* @param {string} paymentPointer The paymentPointer found in a meta tag.
* @return {Promise<boolean>} Whether or not the specified payment pointer is valid.
*/
Expand Down
15 changes: 13 additions & 2 deletions src/data/AkitaOriginData.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,17 @@ class AkitaOriginData {
/**
* @param {{
* paymentPointer: String,
* validationTimestamp: Number,
* amount?: Number,
* assetScale?: Number,
* assetCode?: String
* }} paymentData
* This object may be created, or a Web Monetization event detail object can be used.
* Pass in an object with just a paymentPointer to register a payment pointer for
* the current website. Payment pointer should be validated first.
* Additionally pass in assetCode, assetScale, and amount together to add to the
* Optionally pass in validationTimestamp to set when the payment pointer was most
* recently validated.
* Optionally pass in assetCode, assetScale, and amount together to add to the
* total amount sent to the current website.
*
* assetCode e.g. 'XRP', 'USD', 'CAD'
Expand All @@ -74,14 +77,22 @@ class AkitaOriginData {
updatePaymentData(paymentData) {
if (paymentData) {
const paymentPointer = paymentData.paymentPointer;

if (paymentPointer) {
this.isCurrentlyMonetized = true;

if (!this.paymentPointerMap[paymentPointer]) {
this.paymentPointerMap[paymentPointer] = new AkitaPaymentPointerData(paymentPointer);
}

// Handling for optional argument: validationTimestamp
const validationTimestamp = paymentData.validationTimestamp;

if (validationTimestamp) {
this.paymentPointerMap[paymentPointer].setValidationTimestamp(validationTimestamp);
}

// Handling for the 3 optional arguments: amount, assetScale, and assetCode
const amount = paymentData.amount;
const assetScale = paymentData.assetScale;
const assetCode = paymentData.assetCode;
Expand Down
18 changes: 18 additions & 0 deletions src/data/AkitaPaymentPointerData.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
*/
class AkitaPaymentPointerData {
paymentPointer = null;
// The most recent time (UTC timestamp) when Akita validated the payment pointer
// For more info on payment pointer validation: see ../content_main.js, function isPaymentPointerValid
validationTimestamp = null;
// The type of each entry in sentAssetsMap is: WebMonetizationAsset
sentAssetsMap = {};

Expand All @@ -21,6 +24,7 @@ class AkitaPaymentPointerData {
*/
static fromObject(akitaPaymentPointerDataObject) {
const newPaymentPointerData = new AkitaPaymentPointerData(akitaPaymentPointerDataObject.paymentPointer);
newPaymentPointerData.validationTimestamp = akitaPaymentPointerDataObject.validationTimestamp;

for (const assetCode in akitaPaymentPointerDataObject.sentAssetsMap) {
newPaymentPointerData.sentAssetsMap[assetCode] = WebMonetizationAsset.fromObject(
Expand All @@ -30,6 +34,20 @@ class AkitaPaymentPointerData {
return newPaymentPointerData;
}

/**
* When Akita validates a payment pointer, the time it was validated should be set using this
* function. It is expected that payment pointers are validated by Akita often. In order to make
* sure that validation occurs often, we keep track of the last time the payment pointer was
* validated using validationTimestamp.
*
* For more info on payment pointer validation: see ./main.js, function isPaymentPointerValid
*
* @param {Number} validationTimestamp UTC Timestamp of last time payment pointer was validated.
*/
setValidationTimestamp(validationTimestamp) {
this.validationTimestamp = validationTimestamp;
}

/**
* assetScale and amount e.g.
* if assetCode is 'USD', amount is 235, and assetScale is 3 then actual amount of
Expand Down
5 changes: 4 additions & 1 deletion src/data/storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,21 @@ async function storeDataIntoAkitaFormat(data, typeOfData) {

/**
* Update payment data in originData and in originStats.
*
*
* @param {AkitaOriginData} originData The origin data to update.
* @param {AkitaOriginStats} originStats The origin stats to update.
* @param {{
* paymentPointer: String,
* validationTimestamp: Number,
* amount?: Number,
* assetScale?: Number,
* assetCode?: String
* }} paymentData
* This object may be created, or a Web Monetization event detail object can be used.
* Pass in an object with just a paymentPointer to register a payment pointer for
* the current website. Payment pointer should be validated first.
* Optionally pass in validationTimestamp to set when the payment pointer was most
* recently validated.
* Additionally pass in assetCode, assetScale, and amount together to add to the
* total amount sent to the current website.
*/
Expand Down

0 comments on commit 12545f5

Please sign in to comment.