From 3143d89757b40fccc113701c5064060e18a0bbe0 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:09:18 +0530 Subject: [PATCH 1/4] docs(DEVELOP): describe payment mechanism, new payment services --- cspell-dictionary.txt | 1 + docs/DEVELOP.md | 108 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 100 insertions(+), 9 deletions(-) diff --git a/cspell-dictionary.txt b/cspell-dictionary.txt index 332371173..a4f82d5cf 100644 --- a/cspell-dictionary.txt +++ b/cspell-dictionary.txt @@ -53,6 +53,7 @@ msedge SVGOMG xvfb Titillium +mindmap # user names raducristianpopa diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 26f60d146..70235d40e 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -127,22 +127,47 @@ Each service/component/file is named so that you can have a general idea of what Background script/service worker (it's service worker in Chrome, and script in Firefox at the moment, but there's no specific difference from our perspective) is the backend of our extension. It runs locally on the user's device, not on a remote server. The background script is composed of various services: +```mermaid +mindmap + root{{Background}} + Monetization + PaymentManager + PaymentStream + PaymentSession + (_messaging_) + SendToPopup + EventsService + TabEvents + (_state_) + TabState + WindowState + Storage + (_helpers_) + OpenPayments + KeyAutoAdd + HeartBeat +``` + - **`Background`**: - The "main" entry point for background. - Handles messages from popup and content scripts - Handles post-install & post-update events - Listens to many other "global" events and orchestrates other services to take action. - **`MonetizationService`**: - - The real orchestrator of payments - - Sets up `PaymentSession`s + - The real orchestrator of payments, for all tabs + - Sets up `PaymentManager`s - Handles messages from background/content-scripts/popup to maintain/update/control the payment sessions - - Handles events from different payment sessions -- **`PaymentSession`**: - - Abstraction over `OpenPaymentsService` - - Maintains payment sessions to keep a tab on payments (whether active or can become active) - - Trigger continuous payment streams or send one-time payments - - Publish events to a website where a payment was sent - - Keep track of what amount to send, and when to send. + - **`PaymentManager`** + - Manages payments for a tab + - Abstracts all monetization link elements in a page, manages payment sessions + - Keep track of what amount to send, which session to send to, and when to send + - **`PaymentStream`** + - Contains sessions for link elements in a "frame" within the tab + (host website is main frame (id=0), rest are _iframes_) + - **`PaymentSession`**: + - Abstraction over a monetization link element + - Calls OpenPayments APIs to make actual payments + - Publish events to a website when a payment was sent - **`OpenPaymentsService`**: - An abstraction over OpenPayments client - Preserves tokens, and manages a single client for all operations. @@ -235,6 +260,71 @@ Please read the [MDN docs](https://developer.mozilla.org/en-US/docs/Mozilla/Add- Note that, we only support [manifest version 3](https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3), and not version 2. +### Payment mechanism + +1. The content script finds the valid `` elements and sends a message to the background. +2. The background then sets up a payment session for this element - via `Monetization` (singleton for browser instance) → `PaymentManager` (for tab) → `PaymentStream` (for frame) → `PaymentSession` (for element). +3. Once a payment is made, the `PaymentSession` passes on an event to the website (via `polyfill.ts`), and in turn a `MonetizationEvent` is emitted. + +#### `minSendAmount` + +When a `PaymentSession` is created, we find a `minSendAmount` before we try to make any payment. This value is used in figuring out the amounts and timings for the payments for given link element. The `minSendAmount` is obtained by creating a quote with particular amount and seeing if the quote is accepted at that amount. + +The `minSendAmount` is a property of user's connected wallet, the wallet of the link tag and associated fees at that time. For a browsing session (which _resets_ on navigation), we typically find this amount only once. But if future payments fail, we may need to adjust the `minSendAmount` again. + +We make payments only in multiples of `minSendAmount`. This ensures some fairness for the sender and receivers - as the amounts higher than in-multiple of `minSendAmount` are absorbed by ASEs to maintain cross-currency liquidity for other transactions when user may send an amount less than multiple of `minSendAmount`. + +#### Open-payments flow + +To make a payment with Open Payments, following steps are taken (presuming we've the OutgoingPaymentGrant tokens available, i.e. user has connected the extension to their wallet): + +1. Create incoming payment: + 1. Set up a non-interactive incoming payment grant for the receiving wallet address. + 1. Create an incoming payment using this grant for the receiving wallet address. + 1. Optionally, cancel the incoming payment grant, as we no longer need it (we already have the incoming payment). This prevents users wallet seeing _dangling_ grants in their wallet. +1. Find the `minSendAmount`: + 1. Find a possible amount for the receiving wallet using various heuristics (currency exchange rate, asset scales, asset codes etc.) + 2. Exponential probing: Try creating a quote with this amount until it doesn't fail with a "non-positive receive amount" error (i.e. the receiver has to receive at least one unit for a quote to succeed), increasing it in an exponential manner. + - If the OpenPayments request with a "non-positive receive amount" error, error and includes a `minSendAmount` in error details (this is a relatively recent OpenPayments feature), stop the process to find the minimum sendable amount her1. + 3. Binary search: Once a sendable amount is found, use binary search (between the sendable amount and previously attempted amount) to find the minimum sendable amount. +1. Create outgoing payment: + 1. Call the create outgoing payment OpenPayments API to create a fix-send outgoing payment. Include the following details: + - `accessToken`: The access token for the outgoing payment grant (i.e. the one we get from connecting the wallet). This token needs to be rotated once in a wIfIf the token is expired, try the payment again with a refreshed token. + - `incomingPayment`: The incoming payment ID/URL we got in step 1. + - `debitAmount`: The amount we want to send. + +#### One-time payments + +When a user wants to send one-time payment, we distribute the amount chosen by the user across the _payable_ payment sessions, while respecting the `minSendAmount` of each session. + +We then create multiple payments as described the flow above, to each session that gets a non-zero amount following the above distribution. + +The distribution logic is defined in [#1098](https://github.com/interledger/web-monetization-extension/pull/1098). + +#### Continuous payments + +Continuous payments involve a timing component, managed by `PaymentManager`. + +Depending on the user's chosen rate of pay, we find the interval at which we can increase a "pending amount" that can be paid out. Every "interval" ms, we increment the pending amount by some units. +The interval can't be lower than `MIN_PAYMENT_WAIT` - which defines a minimum time between consecutive payments, for performance reasons. We adjust the interval and increment accordingly. + +From the multiple payment sessions, then we have to choose the session we want to pay next. How sessions are chosen is defined in [#1066](https://github.com/interledger/web-monetization-extension/pull/1066), but essentially we go sequentially in the order we loaded the link elements, while prioritizing the sessions on the "main frame" (host website over iframes). + +
+ +- First, go through all payable link tags on the main website, one by one. +- Then, pay the first link in the first iframe, then the first link in the second iframe. +- Then, again go through all payable link tags on the main website. +- Then, pay the second link of the first iframe, then the second link (if it doesn't exist, then the first again) of the second iframe. +- Then, again go through all payable link tags on the main website. +- Then, again pay the first link in the first iframe, then the first link in the second iframe, and so on. + +
+ +The "pending amount" acts like a bucket, and we empty the bucket by making a payment in a multiple of the chosen session's `minSendAmount` (the bucket isn't always emptied depending on the values of increments and min-send amounts). If there's not enough pending payment, we wait until there is (but still paying the same chosen payment session), and then move onto the next payment session. + +We keep doing this indefinitely (until monetization is stopped by some user action or long inactivity), cycling through the payment sessions. + ## Testing and Debugging ### Unit Testing From 716184df06bdf6700e0d7e5dc77bc6f0320717e6 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:26:22 +0530 Subject: [PATCH 2/4] remove nesting --- docs/DEVELOP.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 70235d40e..9e9520892 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -157,17 +157,17 @@ mindmap - The real orchestrator of payments, for all tabs - Sets up `PaymentManager`s - Handles messages from background/content-scripts/popup to maintain/update/control the payment sessions - - **`PaymentManager`** - - Manages payments for a tab - - Abstracts all monetization link elements in a page, manages payment sessions - - Keep track of what amount to send, which session to send to, and when to send - - **`PaymentStream`** - - Contains sessions for link elements in a "frame" within the tab - (host website is main frame (id=0), rest are _iframes_) - - **`PaymentSession`**: - - Abstraction over a monetization link element - - Calls OpenPayments APIs to make actual payments - - Publish events to a website when a payment was sent +- **`PaymentManager`** + - Manages payments for a tab + - Abstracts all monetization link elements in a page, manages payment sessions + - Keep track of what amount to send, which session to send to, and when to send +- **`PaymentStream`** + - Contains sessions for link elements in a "frame" within the tab + (host website is main frame (id=0), rest are _iframes_) +- **`PaymentSession`**: + - Abstraction over a monetization link element + - Calls OpenPayments APIs to make actual payments + - Publish events to a website when a payment was sent - **`OpenPaymentsService`**: - An abstraction over OpenPayments client - Preserves tokens, and manages a single client for all operations. From 937f1cfe4e8daa180d5caf47a6c9363a78b1f409 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Mon, 23 Jun 2025 18:49:43 +0530 Subject: [PATCH 3/4] nit --- docs/DEVELOP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 9e9520892..1b0463691 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -270,7 +270,7 @@ Note that, we only support [manifest version 3](https://developer.chrome.com/doc When a `PaymentSession` is created, we find a `minSendAmount` before we try to make any payment. This value is used in figuring out the amounts and timings for the payments for given link element. The `minSendAmount` is obtained by creating a quote with particular amount and seeing if the quote is accepted at that amount. -The `minSendAmount` is a property of user's connected wallet, the wallet of the link tag and associated fees at that time. For a browsing session (which _resets_ on navigation), we typically find this amount only once. But if future payments fail, we may need to adjust the `minSendAmount` again. +The `minSendAmount` is a property of user's connected wallet (currency/assetScale), the wallet for the given link element and associated payment fees at that time. For a browsing session (which _resets_ on navigation), we typically find this amount only once. But if future payments fail, we may need to adjust the `minSendAmount` again. We make payments only in multiples of `minSendAmount`. This ensures some fairness for the sender and receivers - as the amounts higher than in-multiple of `minSendAmount` are absorbed by ASEs to maintain cross-currency liquidity for other transactions when user may send an amount less than multiple of `minSendAmount`. From bc0e297bf9967074e56a427ae3dfa33120c97f05 Mon Sep 17 00:00:00 2001 From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com> Date: Tue, 24 Jun 2025 15:31:52 +0530 Subject: [PATCH 4/4] add missing wallet service as well --- docs/DEVELOP.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/DEVELOP.md b/docs/DEVELOP.md index 1b0463691..78dcc226e 100644 --- a/docs/DEVELOP.md +++ b/docs/DEVELOP.md @@ -144,8 +144,9 @@ mindmap Storage (_helpers_) OpenPayments - KeyAutoAdd HeartBeat + Wallet + KeyAutoAdd ``` - **`Background`**: @@ -190,6 +191,9 @@ mindmap - **`SendToPopup`**: - Send messages from the background to popup via `Runtime.Port` - Exists as we send messages to the popup often, and sending via a regular message channel would be noisy. +- **`WalletService`** + - Handles wallet connection, budget updates, disconnecting and reconnecting wallet + - Uses `KeyAutoAddService` to add keys during connection - **`KeyAutoAddService`**: - Different wallets have different ways to upload public keys, and we don't want users to be scared by the complexity of "public keys" - This service complimented by its counterpart content scripts, takes control of the user's browser (after their consent) to add the key via reverse engineering how different wallets upload keys.