Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add periodic tracking and periodic validation for payment pointers #84

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 127 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,12 @@ 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) {
vezwork marked this conversation as resolved.
Show resolved Hide resolved
const webBrowser = chrome ? chrome : browser;
webBrowser.runtime.sendMessage({ isCurrentlyMonetized });
}
}

/***********************************************************
Expand Down Expand Up @@ -114,12 +112,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 +163,135 @@ 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.
* 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:
vezwork marked this conversation as resolved.
Show resolved Hide resolved
* - 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.
*/
function getPaymentPointerFromPage() {
const monetizationMetaTag = document.querySelector('meta[name="monetization"]');

if (monetizationMetaTag) {
vezwork marked this conversation as resolved.
Show resolved Hide resolved
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) {
vezwork marked this conversation as resolved.
Show resolved Hide resolved
// 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.
*/
async function getAndValidatePaymentPointer() {
const monetizationMeta = document.querySelector('meta[name="monetization"]');
let paymentPointer = (monetizationMeta) ? monetizationMeta.content : null;
let isValid = false;
function handleUpdatingExtensionIconOnTabVisibilityChange() {
document.addEventListener('visibilitychange', (event) => {
const tabIsVisible = !document.hidden;

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

if (null === monetizationMeta) {
/* No monetization meta tag provided */
/**
* 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:
vezwork marked this conversation as resolved.
Show resolved Hide resolved
* - 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
20 changes: 20 additions & 0 deletions src/data/AkitaPaymentPointerData.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
*/
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;
vezwork marked this conversation as resolved.
Show resolved Hide resolved

// The type of each entry in sentAssetsMap is: WebMonetizationAsset
sentAssetsMap = {};

Expand All @@ -21,6 +26,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 +36,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
vezwork marked this conversation as resolved.
Show resolved Hide resolved
*
* @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