diff --git a/.husky/pre-commit b/.husky/pre-commit
index 3725dedca..54ba576f1 100755
--- a/.husky/pre-commit
+++ b/.husky/pre-commit
@@ -2,4 +2,4 @@
. "$(dirname -- "$0")/_/husky.sh"
yarn fmt
-git add -A
\ No newline at end of file
+git add -A
diff --git a/assets/_locales/en/messages.json b/assets/_locales/en/messages.json
index 596c707ce..171e2933f 100644
--- a/assets/_locales/en/messages.json
+++ b/assets/_locales/en/messages.json
@@ -615,11 +615,11 @@
},
"enter_new_password": {
"message": "Enter your new password...",
- "description": "Password input placeholder"
+ "description": "Password input placeholder"
},
"reenter_new_password": {
"message": "Re-enter your new password...",
- "description": "Re-enter input placeholder"
+ "description": "Re-enter input placeholder"
},
"enter_password": {
"message": "Enter your password...",
@@ -1333,12 +1333,16 @@
"message": "Click to view your last signed transaction",
"description": "View last tx prompt content"
},
+ "view_transaction_details": {
+ "message": "View transaction details",
+ "description": "View transaction details link text"
+ },
"sign_item": {
- "message": "Sign Item",
+ "message": "Sign item",
"description": "Sign message popup title"
},
"batch_sign_items": {
- "message": "Batch Sign Items",
+ "message": "Sign items",
"description": "Batch sign message popup title"
},
"titles_signature": {
@@ -1391,6 +1395,10 @@
"message": "Authorize",
"description": "Authorize button label"
},
+ "sign_authorize_all": {
+ "message": "Authorize all",
+ "description": "Authorize all button label"
+ },
"sign_enter_password": {
"message": "Enter password to sign transaction",
"description": "Request to enter password to sign transaction"
@@ -1604,7 +1612,7 @@
"description": "Lock wallet and terminate current session icon button"
},
"home": {
- "message":"Home",
+ "message":"Home",
"description": "Home Button"
},
"home_no_balance": {
@@ -1673,11 +1681,11 @@
"message": "Toggle to receive alerts for new transactions in your wallet.",
"description": "Toggle notifications description text"
},
- "get_started":{
+ "get_started":{
"message": "Get Started",
"description": "Get started title"
},
- "get_started_description":{
+ "get_started_description":{
"message": "Explore the endless possibilities on Arweave and aoComputer through ArConnect",
"description": "Get started description"
},
@@ -1806,7 +1814,7 @@
"description": "Currency search placeholder text"
},
"receive_AR_button": {
- "message": "Receive AR",
+ "message": "Receive AR",
"description": "Button text for the receive AR button"
},
"search_contacts": {
@@ -1925,7 +1933,7 @@
"description": "Input title for the password allowance label"
},
"add": {
- "message": "Add",
+ "message": "Add",
"description" : "add button text"
},
"your_contacts": {
@@ -1941,24 +1949,24 @@
"description": "Error message for invalid ANS"
},
"arns_added": {
- "message": " ar://$ADDRESS$ added",
+ "message": " ar://$ADDRESS$ added",
"description": "ArNS recipient added",
"placeholders": {
"address": {
"content": "$1",
"example": "Address"
}
- }
+ }
},
"ans_added": {
- "message": "$ADDRESS$ added",
+ "message": "$ADDRESS$ added",
"description": "ANS recipient added",
"placeholders": {
"address": {
"content": "$1",
"example": "Address"
}
- }
+ }
},
"setting_ao_support": {
"message": "ao support",
@@ -2110,7 +2118,7 @@
"description": "Address where payment will be made to"
},
-
+
"subscription_recurring_amount": {
"message": "Recurring payment amount",
"description": "Recurring payment amount"
@@ -2128,12 +2136,12 @@
"message": "Confirm Subscription",
"description": "confirm subscription"
},
-
+
"continue": {
"message": "Continue",
"description": "Continue"
},
-
+
"end": {
"message": "End",
"description": "end"
@@ -2285,5 +2293,95 @@
"new_transaction": {
"message": "New transaction",
"description": "New transaction description"
+ },
+ "batchSignDataItemRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type batchSignDataItem"
+ },
+ "connectRequestLoading": {
+ "message": "Connecting...",
+ "description": "Loading message for AuthRequest of type connect"
+ },
+ "allowanceRequestLoading": {
+ "message": "Updating allowance...",
+ "description": "Loading message for AuthRequest of type allowance"
+ },
+ "tokenRequestLoading": {
+ "message": "Adding token...",
+ "description": "Loading message for AuthRequest of type token"
+ },
+ "signRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type sign"
+ },
+ "subscriptionRequestLoading": {
+ "message": "Subscribing...",
+ "description": "Loading message for AuthRequest of type subscription"
+ },
+ "signKeystoneRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type signKeystone"
+ },
+ "signatureRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type signature"
+ },
+ "signDataItemRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type signDataItem"
+ },
+ "abortingRequestLoading": {
+ "message": "Aborting...",
+ "description": "Loading message for rejected/aborted AuthRequests"
+ },
+ "pendingTransactionStatusAt": {
+ "message": "Requested",
+ "description": "Label to display next to the elapsed time since the AuthRequest was created"
+ },
+ "acceptedTransactionStatusAt": {
+ "message": "Accepted",
+ "description": "Label to display next to the elapsed time since the AuthRequest was accepted"
+ },
+ "rejectedTransactionStatusAt": {
+ "message": "Rejected",
+ "description": "Label to display next to the elapsed time since the AuthRequest was rejected"
+ },
+ "abortedTransactionStatusAt": {
+ "message": "Aborted",
+ "description": "Label to display next to the elapsed time since the AuthRequest was aborted"
+ },
+ "errorTransactionStatusAt": {
+ "message": "Failed",
+ "description": "Label to display next to the elapsed time since the AuthRequest was aborted"
+ },
+ "formattedElapsedSeconds": {
+ "message": "$SECONDS$ seconds ago",
+ "description": "Format elapsed seconds",
+ "placeholders": {
+ "seconds": {
+ "content": "$1",
+ "example": "10"
+ }
+ }
+ },
+ "formattedElapsedMinutes": {
+ "message": "$MINUTES$ minutes ago",
+ "description": "Format elapsed minutes",
+ "placeholders": {
+ "minutes": {
+ "content": "$1",
+ "example": "10"
+ }
+ }
+ },
+ "formattedElapsedHours": {
+ "message": "$HOURS$ hours ago",
+ "description": "Format elapsed hours",
+ "placeholders": {
+ "hours": {
+ "content": "$1",
+ "example": "10"
+ }
+ }
}
}
diff --git a/assets/_locales/zh_CN/messages.json b/assets/_locales/zh_CN/messages.json
index 6efe1cbca..c931c6d01 100644
--- a/assets/_locales/zh_CN/messages.json
+++ b/assets/_locales/zh_CN/messages.json
@@ -1321,6 +1321,10 @@
"message": "点击查看您最近签署的交易",
"description": "View last tx prompt content"
},
+ "view_transaction_details": {
+ "message": "查看交易详情",
+ "description": "View transaction details link text"
+ },
"sign_item": {
"message": "签署项目",
"description": "Sign message popup title"
@@ -1379,6 +1383,10 @@
"message": "授权",
"description": "Authorize button label"
},
+ "sign_authorize_all": {
+ "message": "Authorize all",
+ "description": "Authorize all button label"
+ },
"sign_enter_password": {
"message": "输入密码以签署交易",
"description": "Request to enter password to sign transaction"
@@ -2271,5 +2279,95 @@
"new_transaction": {
"message": "新交易",
"description": "New transaction description"
+ },
+ "batchSignDataItemRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type batchSignDataItem"
+ },
+ "connectRequestLoading": {
+ "message": "Connecting...",
+ "description": "Loading message for AuthRequest of type connect"
+ },
+ "allowanceRequestLoading": {
+ "message": "Updating allowance...",
+ "description": "Loading message for AuthRequest of type allowance"
+ },
+ "tokenRequestLoading": {
+ "message": "Adding token...",
+ "description": "Loading message for AuthRequest of type token"
+ },
+ "signRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type sign"
+ },
+ "subscriptionRequestLoading": {
+ "message": "Subscribing...",
+ "description": "Loading message for AuthRequest of type subscription"
+ },
+ "signKeystoneRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type signKeystone"
+ },
+ "signatureRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type signature"
+ },
+ "signDataItemRequestLoading": {
+ "message": "Signing...",
+ "description": "Loading message for AuthRequest of type signDataItem"
+ },
+ "abortingRequestLoading": {
+ "message": "Aborting...",
+ "description": "Loading message for rejected/aborted AuthRequests"
+ },
+ "pendingTransactionStatusAt": {
+ "message": "Requested",
+ "description": "Label to display next to the elapsed time since the AuthRequest was created"
+ },
+ "acceptedTransactionStatusAt": {
+ "message": "Accepted",
+ "description": "Label to display next to the elapsed time since the AuthRequest was accepted"
+ },
+ "rejectedTransactionStatusAt": {
+ "message": "Rejected",
+ "description": "Label to display next to the elapsed time since the AuthRequest was rejected"
+ },
+ "abortedTransactionStatusAt": {
+ "message": "Aborted",
+ "description": "Label to display next to the elapsed time since the AuthRequest was aborted"
+ },
+ "errorTransactionStatusAt": {
+ "message": "Failed",
+ "description": "Label to display next to the elapsed time since the AuthRequest was aborted"
+ },
+ "formattedElapsedSeconds": {
+ "message": "$SECONDS$ seconds ago",
+ "description": "Format elapsed seconds",
+ "placeholders": {
+ "seconds": {
+ "content": "$1",
+ "example": "10"
+ }
+ }
+ },
+ "formattedElapsedMinutes": {
+ "message": "$MINUTES$ minutes ago",
+ "description": "Format elapsed minutes",
+ "placeholders": {
+ "minutes": {
+ "content": "$1",
+ "example": "10"
+ }
+ }
+ },
+ "formattedElapsedHours": {
+ "message": "$HOURS$ hours ago",
+ "description": "Format elapsed hours",
+ "placeholders": {
+ "hours": {
+ "content": "$1",
+ "example": "10"
+ }
+ }
}
}
diff --git a/assets/placeholder.png b/assets/placeholder.png
new file mode 100644
index 000000000..9c4d05cf7
Binary files /dev/null and b/assets/placeholder.png differ
diff --git a/assets/popup.css b/assets/popup.css
index 7ce859145..78780670b 100644
--- a/assets/popup.css
+++ b/assets/popup.css
@@ -55,17 +55,21 @@
:root {
--defaultBackgroundColor: white;
+ --defaultTextColor: black;
}
@media (prefers-color-scheme: dark) {
:root {
--defaultBackgroundColor: black;
+ --defaultTextColor: white;
}
}
body {
margin: 0;
padding: 0;
+ background-color: var(--backgroundColor, var(--defaultBackgroundColor, white));
+ color: var(--textColor, var(--defaultTextColor, black));
}
body#popup {
@@ -97,6 +101,7 @@ input,
select,
textarea {
font-family: "ManropeLocal", "Manrope VF", "Manrope", sans-serif !important;
+ letter-spacing: .5px;
}
button {
diff --git a/assets/popup.js b/assets/popup.js
index 7c36b0bcb..a49ef20f3 100644
--- a/assets/popup.js
+++ b/assets/popup.js
@@ -1,3 +1,5 @@
const backgroundColor = localStorage.getItem("ARCONNECT_THEME_BACKGROUND_COLOR");
-
-if (backgroundColor) document.documentElement.style.setProperty('--backgroundColor', backgroundColor);
\ No newline at end of file
+const textColor = localStorage.getItem("ARCONNECT_THEME_TEXT_COLOR");
+
+if (backgroundColor) document.documentElement.style.setProperty('--backgroundColor', backgroundColor);
+if (textColor) document.documentElement.style.setProperty('--textColor', textColor);
diff --git a/index.html b/index.html
new file mode 100644
index 000000000..789fde4aa
--- /dev/null
+++ b/index.html
@@ -0,0 +1,14 @@
+
+
+
+ ArConnect Embedded Wallet
+
+
+
+
+
+
+
+
+
+
diff --git a/package.json b/package.json
index 23c9ac159..61b19a4b0 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
]
},
"dependencies": {
- "@arconnect/components": "^0.3.11",
+ "@arconnect/components": "^1.0.0",
"@arconnect/keystone-sdk": "^0.0.5",
"@arconnect/warp-dre": "^0.0.1",
"@arconnect/webext-bridge": "^5.0.6",
@@ -60,6 +60,7 @@
"@permaweb/aoconnect": "^0.0.55",
"@plasmohq/storage": "^1.7.2",
"@segment/analytics-next": "^1.53.2",
+ "@swyg/corre": "^1.0.4",
"@untitled-ui/icons-react": "^0.1.1",
"ao-tokens": "^0.0.4",
"ar-gql": "1.2.9",
@@ -76,7 +77,6 @@
"js-confetti": "^0.11.0",
"mitt": "^3.0.0",
"nanoid": "^4.0.0",
- "path-to-regexp": "^6.2.1",
"plimit-lit": "^3.0.1",
"pretty-bytes": "^6.0.0",
"qrcode.react": "^3.1.0",
@@ -92,7 +92,7 @@
"uuid": "^9.0.0",
"warp-contracts": "^1.2.13",
"webextension-polyfill": "^0.10.0",
- "wouter": "^2.8.0-alpha.2"
+ "wouter": "^3.3.5"
},
"devDependencies": {
"@changesets/cli": "^2.27.1",
diff --git a/shim.d.ts b/shim.d.ts
index 2921c8afc..db972077d 100644
--- a/shim.d.ts
+++ b/shim.d.ts
@@ -3,17 +3,66 @@ import type { DisplayTheme } from "@arconnect/components";
import type { Chunk } from "~api/modules/sign/chunks";
import type { InjectedEvents } from "~utils/events";
import "styled-components";
+import type {
+ AuthRequestMessageData,
+ AuthResult
+} from "~utils/auth/auth.types";
declare module "@arconnect/webext-bridge" {
export interface ProtocolMap {
+ /**
+ * `api/foreground/foreground-setup-wallet-sdk.ts` use `postMessage()` to send `arweaveWallet.*` calls that are
+ * received in `contents/api.ts`, which then sends them to the background using `sendMessage()`.
+ */
api_call: ProtocolWithReturn;
- auth_result: AuthResult;
+
+ /**
+ * `dispatch.foreground.ts` and `sign.foreground.ts` use `sendChunk()` to send chunks to the background.
+ */
+ chunk: ProtocolWithReturn, ApiResponse>;
+
+ /**
+ * `createAuthPopup()` in `auth.utils.ts` sends `auth_request` messages from the background to the auth popup, which
+ * are received in `auth.provider.ts`.
+ */
+ auth_request: AuthRequestMessageData;
+
+ /**
+ * `auth.hook.ts` uses `auth_result` messages (calling `replyToAuthRequest()`) to reply to the `AuthRequest`s.
+ */
+ auth_result: AuthResult;
+
+ /**
+ * `signAuth()` in `sign_auth.ts` uses `auth_chunk` to send chunked transactions or binary data from the background
+ * to the auth popup.
+ */
+ auth_chunk: Chunk;
+
+ /**
+ * The background sends `auth_tab_closed` messages to notify the auth popup of closed tabs.
+ */
+ auth_tab_closed: number;
+
+ /**
+ * The background sends `auth_tab_reloaded` messages to notify the auth popup of reloaded tabs.
+ */
+ auth_tab_reloaded: number;
+
+ /**
+ * The background sends `auth_active_wallet_change` messages to notify the auth popup of active wallet changes.
+ */
+ auth_active_wallet_change: number;
+
+ /**
+ * The background sends `auth_app_disconnected` messages to notify the auth popup of disconnected apps.
+ */
+ auth_app_disconnected: number;
+
+ // OTHER:
+
switch_wallet_event: string | null;
copy_address: string;
- chunk: ProtocolWithReturn, ApiResponse>;
event: Event;
- auth_listening: number;
- auth_chunk: Chunk;
ar_protocol: ProtocolWithReturn<{ url: string }, { url: sting }>;
}
}
@@ -28,13 +77,6 @@ interface ApiResponse extends ApiCall {
error?: boolean;
}
-interface AuthResult {
- type: string;
- authID: string;
- error?: boolean;
- data?: DataType;
-}
-
interface Event {
name: keyof InjectedEvents;
value: unknown;
diff --git a/src/api/background.ts b/src/api/background.ts
deleted file mode 100644
index 2b5df2526..000000000
--- a/src/api/background.ts
+++ /dev/null
@@ -1,96 +0,0 @@
-import type { Module } from "./module";
-
-// import modules
-import permissionsModule from "./modules/permissions";
-import permissions from "./modules/permissions/permissions.background";
-import activeAddressModule from "./modules/active_address";
-import activeAddress from "./modules/active_address/active_address.background";
-import allAddressesModule from "./modules/all_addresses";
-import allAddresses from "./modules/all_addresses/all_addresses.background";
-import publicKeyModule from "./modules/public_key";
-import publicKey from "./modules/public_key/public_key.background";
-import walletNamesModule from "./modules/wallet_names";
-import walletNames from "./modules/wallet_names/wallet_names.background";
-import arweaveConfigModule from "./modules/arweave_config";
-import arweaveConfig from "./modules/arweave_config/arweave_config.background";
-import disconnectModule from "./modules/disconnect";
-import disconnect from "./modules/disconnect/disconnect.background";
-import connectModule from "./modules/connect";
-import connect from "./modules/connect/connect.background";
-import signModule from "./modules/sign";
-import sign from "./modules/sign/sign.background";
-import dispatchModule from "./modules/dispatch";
-import dispatch from "./modules/dispatch/dispatch.background";
-import encryptModule from "./modules/encrypt";
-import encrypt from "./modules/encrypt/encrypt.background";
-import decryptModule from "./modules/decrypt";
-import decrypt from "./modules/decrypt/decrypt.background";
-import signatureModule from "./modules/signature";
-import signature from "./modules/signature/signature.background";
-import addTokenModule from "./modules/add_token";
-import addToken from "./modules/add_token/add_token.background";
-import isTokenAddedModule from "./modules/is_token_added";
-import isTokenAdded from "./modules/is_token_added/is_token_added.background";
-import signMessageModule from "./modules/sign_message";
-import signMessage from "./modules/sign_message/sign_message.background";
-import privateHashModule from "./modules/private_hash";
-import privateHash from "./modules/private_hash/private_hash.background";
-import verifyMessageModule from "./modules/verify_message";
-import verifyMessage from "./modules/verify_message/verify_message.background";
-import signDataItemModule from "./modules/sign_data_item";
-import signDataItem from "./modules/sign_data_item/sign_data_item.background";
-import batchSignDataItemModule from "./modules/batch_sign_data_item";
-import batchSignDataItem from "./modules/batch_sign_data_item/batch_sign_data_item.background";
-import subscriptionModule from "./modules/subscription";
-import subscription from "./modules/subscription/subscription.background";
-import userTokensModule from "./modules/user_tokens";
-import userTokens from "./modules/user_tokens/user_tokens.background";
-import tokenBalanceModule from "./modules/token_balance";
-import tokenBalance from "./modules/token_balance/token_balance.background";
-
-/** Background modules */
-const modules: BackgroundModule[] = [
- { ...permissionsModule, function: permissions },
- { ...activeAddressModule, function: activeAddress },
- { ...allAddressesModule, function: allAddresses },
- { ...publicKeyModule, function: publicKey },
- { ...walletNamesModule, function: walletNames },
- { ...arweaveConfigModule, function: arweaveConfig },
- { ...disconnectModule, function: disconnect },
- { ...connectModule, function: connect },
- { ...addTokenModule, function: addToken },
- { ...isTokenAddedModule, function: isTokenAdded },
- { ...signModule, function: sign },
- { ...dispatchModule, function: dispatch },
- { ...encryptModule, function: encrypt },
- { ...decryptModule, function: decrypt },
- { ...signatureModule, function: signature },
- { ...signMessageModule, function: signMessage },
- { ...privateHashModule, function: privateHash },
- { ...verifyMessageModule, function: verifyMessage },
- { ...signDataItemModule, function: signDataItem },
- { ...subscriptionModule, function: subscription },
- { ...userTokensModule, function: userTokens },
- { ...tokenBalanceModule, function: tokenBalance },
- { ...batchSignDataItemModule, function: batchSignDataItem }
-];
-
-export default modules;
-
-/** Extended module interface */
-interface BackgroundModule extends Module {
- function: ModuleFunction;
-}
-
-/**
- * Extended module function
- */
-export type ModuleFunction = (
- appData: ModuleAppData,
- ...params: any[]
-) => Promise | ResultType;
-
-export interface ModuleAppData {
- appURL: string;
- favicon?: string;
-}
diff --git a/src/api/background/background-modules.ts b/src/api/background/background-modules.ts
new file mode 100644
index 000000000..d0ed50630
--- /dev/null
+++ b/src/api/background/background-modules.ts
@@ -0,0 +1,95 @@
+import type { Module } from "../module";
+
+// import modules
+import permissionsModule from "../modules/permissions";
+import permissions from "../modules/permissions/permissions.background";
+import activeAddressModule from "../modules/active_address";
+import activeAddress from "../modules/active_address/active_address.background";
+import allAddressesModule from "../modules/all_addresses";
+import allAddresses from "../modules/all_addresses/all_addresses.background";
+import publicKeyModule from "../modules/public_key";
+import publicKey from "../modules/public_key/public_key.background";
+import walletNamesModule from "../modules/wallet_names";
+import walletNames from "../modules/wallet_names/wallet_names.background";
+import arweaveConfigModule from "../modules/arweave_config";
+import arweaveConfig from "../modules/arweave_config/arweave_config.background";
+import disconnectModule from "../modules/disconnect";
+import disconnect from "../modules/disconnect/disconnect.background";
+import connectModule from "../modules/connect";
+import connect from "../modules/connect/connect.background";
+import signModule from "../modules/sign";
+import sign from "../modules/sign/sign.background";
+import dispatchModule from "../modules/dispatch";
+import dispatch from "../modules/dispatch/dispatch.background";
+import encryptModule from "../modules/encrypt";
+import encrypt from "../modules/encrypt/encrypt.background";
+import decryptModule from "../modules/decrypt";
+import decrypt from "../modules/decrypt/decrypt.background";
+import signatureModule from "../modules/signature";
+import signature from "../modules/signature/signature.background";
+import addTokenModule from "../modules/add_token";
+import addToken from "../modules/add_token/add_token.background";
+import isTokenAddedModule from "../modules/is_token_added";
+import isTokenAdded from "../modules/is_token_added/is_token_added.background";
+import signMessageModule from "../modules/sign_message";
+import signMessage from "../modules/sign_message/sign_message.background";
+import privateHashModule from "../modules/private_hash";
+import privateHash from "../modules/private_hash/private_hash.background";
+import verifyMessageModule from "../modules/verify_message";
+import verifyMessage from "../modules/verify_message/verify_message.background";
+import signDataItemModule from "../modules/sign_data_item";
+import signDataItem from "../modules/sign_data_item/sign_data_item.background";
+import batchSignDataItemModule from "../modules/batch_sign_data_item";
+import batchSignDataItem from "../modules/batch_sign_data_item/batch_sign_data_item.background";
+import subscriptionModule from "../modules/subscription";
+import subscription from "../modules/subscription/subscription.background";
+import userTokensModule from "../modules/user_tokens";
+import userTokens from "../modules/user_tokens/user_tokens.background";
+import tokenBalanceModule from "../modules/token_balance";
+import tokenBalance from "../modules/token_balance/token_balance.background";
+
+export interface ModuleAppData {
+ tabID: number;
+ url: string;
+ favicon?: string;
+}
+
+/**
+ * Extended module function
+ */
+export type BackgroundModuleFunction = (
+ appData: ModuleAppData,
+ ...params: any[]
+) => Promise | ResultType;
+
+/** Extended module interface */
+export interface BackgroundModule extends Module {
+ function: BackgroundModuleFunction;
+}
+
+/** Background modules */
+export const backgroundModules: BackgroundModule[] = [
+ { ...permissionsModule, function: permissions },
+ { ...activeAddressModule, function: activeAddress },
+ { ...allAddressesModule, function: allAddresses },
+ { ...publicKeyModule, function: publicKey },
+ { ...walletNamesModule, function: walletNames },
+ { ...arweaveConfigModule, function: arweaveConfig },
+ { ...disconnectModule, function: disconnect },
+ { ...connectModule, function: connect },
+ { ...addTokenModule, function: addToken },
+ { ...isTokenAddedModule, function: isTokenAdded },
+ { ...signModule, function: sign },
+ { ...dispatchModule, function: dispatch },
+ { ...encryptModule, function: encrypt },
+ { ...decryptModule, function: decrypt },
+ { ...signatureModule, function: signature },
+ { ...signMessageModule, function: signMessage },
+ { ...privateHashModule, function: privateHash },
+ { ...verifyMessageModule, function: verifyMessage },
+ { ...signDataItemModule, function: signDataItem },
+ { ...subscriptionModule, function: subscription },
+ { ...userTokensModule, function: userTokens },
+ { ...tokenBalanceModule, function: tokenBalance },
+ { ...batchSignDataItemModule, function: batchSignDataItem }
+];
diff --git a/src/api/background/background-setup.ts b/src/api/background/background-setup.ts
new file mode 100644
index 000000000..c99cdb17e
--- /dev/null
+++ b/src/api/background/background-setup.ts
@@ -0,0 +1,115 @@
+import { onMessage } from "@arconnect/webext-bridge";
+import handleFeeAlarm from "~api/modules/sign/fee";
+import { ExtensionStorage } from "~utils/storage";
+import browser from "webextension-polyfill";
+import { handleApiCallMessage } from "~api/background/handlers/message/api-call-message/api-call-message.handler";
+import { handleChunkMessage } from "~api/background/handlers/message/chunk-message/chunk-message.handler";
+import { handleInstall } from "~api/background/handlers/browser/install/install.handler";
+import { handleProtocol } from "~api/background/handlers/browser/protocol/protocol.handler";
+import { handleActiveAddressChange } from "~api/background/handlers/storage/active-address-change/active-address-change.handler";
+import { handleWalletsChange } from "~api/background/handlers/storage/wallet-change/wallet-change.handler";
+import { handleAppsChange } from "~api/background/handlers/storage/apps-change/app-change.handler";
+import { handleAppConfigChange } from "~api/background/handlers/storage/app-config-change/app-config-change.handler";
+import { handleTrackBalanceAlarm } from "~api/background/handlers/alarms/track-balance/track-balance-alarm.handler";
+import { handleGetPrinters } from "~api/background/handlers/browser/printer/get-printers/get-printers.handler";
+import { handleGetCapabilities } from "~api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler";
+import { handlePrint } from "~api/background/handlers/browser/printer/print/print.handler";
+import { handleNotificationsAlarm } from "~api/background/handlers/alarms/notifications/notifications-alarm.handler";
+import { handleSubscriptionsAlarm } from "~api/background/handlers/alarms/subscriptions/subscriptions-alarm.handler";
+import { handleAoTokenCacheAlarm } from "~api/background/handlers/alarms/ao-tokens-cache/ao-tokens-cache-alarm.handler";
+import { handleGatewayUpdateAlarm } from "~api/background/handlers/alarms/gateway-update/gateway-update-alarm.handler";
+import { handleSyncLabelsAlarm } from "~api/background/handlers/alarms/sync-labels/sync-labels-alarm.handler";
+import { handleWindowClose } from "~api/background/handlers/browser/window-close/window-close.handler";
+import { handleKeyRemovalAlarm } from "~api/background/handlers/alarms/key-removal/key-removal-alarm.handler";
+import { handleAoTokensImportAlarm } from "~api/background/handlers/alarms/ao-tokens-import/ao-tokens-import-alarm.handler";
+import {
+ handleTabClosed,
+ handleTabUpdate
+} from "~api/background/handlers/browser/tabs/tabs.handler";
+import { log, LOG_GROUP } from "~utils/log/log.utils";
+
+export function setupBackgroundService() {
+ log(
+ LOG_GROUP.SETUP,
+ `background-setup.ts > setupBackgroundService(PLASMO_PUBLIC_APP_TYPE = "${process.env.PLASMO_PUBLIC_APP_TYPE}")`
+ );
+
+ // MESSAGES:
+ // Watch for API call and chunk messages:
+ onMessage("api_call", handleApiCallMessage);
+ onMessage("chunk", handleChunkMessage);
+
+ // LIFECYCLE:
+
+ // Open welcome page on extension install.
+ browser.runtime.onInstalled.addListener(handleInstall);
+
+ // ALARMS:
+ browser.alarms.onAlarm.addListener(handleNotificationsAlarm);
+ browser.alarms.onAlarm.addListener(handleSubscriptionsAlarm);
+ browser.alarms.onAlarm.addListener(handleTrackBalanceAlarm);
+ browser.alarms.onAlarm.addListener(handleGatewayUpdateAlarm);
+ browser.alarms.onAlarm.addListener(handleSyncLabelsAlarm);
+ browser.alarms.onAlarm.addListener(handleKeyRemovalAlarm);
+ browser.alarms.onAlarm.addListener(handleAoTokenCacheAlarm);
+ browser.alarms.onAlarm.addListener(handleAoTokensImportAlarm);
+
+ // handle keep alive alarm
+ browser.alarms.onAlarm.addListener((alarm) => {
+ if (alarm.name === "keep-alive") {
+ console.log("keep alive alarm");
+ }
+ });
+
+ // handle fee alarm (send fees asynchronously)
+ // browser.alarms.onAlarm.addListener(handleFeeAlarm);
+
+ // STORAGE:
+
+ // watch for active address changes / app
+ // list changes
+ // and send them to the content script to
+ // fire the wallet switch event
+ ExtensionStorage.watch({
+ apps: handleAppsChange,
+ active_address: handleActiveAddressChange,
+ wallets: handleWalletsChange
+ });
+
+ // listen for app config updates
+ // `ExtensionStorage.watch` requires a callbackMap param, so this cannot be done using `ExtensionStorage` directly.
+ browser.storage.onChanged.addListener(handleAppConfigChange);
+
+ if (process.env.PLASMO_PUBLIC_APP_TYPE !== "extension") return;
+
+ // ONLY BROWSER EXTENSION BELOW THIS LINE:
+
+ // When the last window connected to the extension is closed, the decryption key will be removed from memory. This is no needed in the embedded wallet because
+ // each wallet instance will be removed automatically when its tab/window is closed.
+ browser.windows.onRemoved.addListener(handleWindowClose);
+
+ // handle tab change (icon, context menus)
+ browser.tabs.onUpdated.addListener((tabId, changeInfo) => {
+ handleTabUpdate(tabId, changeInfo);
+ });
+ browser.tabs.onActivated.addListener(({ tabId }) => {
+ handleTabUpdate(tabId);
+ });
+ browser.tabs.onRemoved.addListener(handleTabClosed);
+
+ // handle ar:// protocol
+ browser.webNavigation.onBeforeNavigate.addListener(handleProtocol);
+
+ // print to the permaweb (only on chrome)
+ if (typeof chrome !== "undefined") {
+ chrome.printerProvider.onGetCapabilityRequested.addListener(
+ handleGetCapabilities
+ );
+
+ chrome.printerProvider.onGetPrintersRequested.addListener(
+ handleGetPrinters
+ );
+
+ chrome.printerProvider.onPrintRequested.addListener(handlePrint);
+ }
+}
diff --git a/src/api/background/handlers/alarms/ao-tokens-cache/ao-tokens-cache-alarm.handler.ts b/src/api/background/handlers/alarms/ao-tokens-cache/ao-tokens-cache-alarm.handler.ts
new file mode 100644
index 000000000..bec7bf48f
--- /dev/null
+++ b/src/api/background/handlers/alarms/ao-tokens-cache/ao-tokens-cache-alarm.handler.ts
@@ -0,0 +1,52 @@
+import { dryrun } from "@permaweb/aoconnect";
+import { ExtensionStorage } from "~utils/storage";
+import type { Alarms } from "webextension-polyfill";
+import { Id, Owner, type TokenInfo } from "~tokens/aoTokens/ao";
+import { timeoutPromise } from "~utils/promises/timeout";
+import { getTokenInfoFromData } from "~tokens/aoTokens/router";
+
+/**
+ * Alarm handler for syncing ao tokens
+ */
+export const handleAoTokenCacheAlarm = async (alarmInfo?: Alarms.Alarm) => {
+ if (alarmInfo && !alarmInfo.name.startsWith("update_ao_tokens")) return;
+
+ const aoTokens = (await ExtensionStorage.get("ao_tokens")) || [];
+
+ const updatedTokens = [...aoTokens];
+
+ for (const token of aoTokens) {
+ try {
+ const res = await timeoutPromise(
+ dryrun({
+ Id,
+ Owner,
+ process: token.processId,
+ tags: [{ name: "Action", value: "Info" }]
+ }),
+ 6000
+ );
+
+ if (res.Messages && Array.isArray(res.Messages)) {
+ const tokenInfo = getTokenInfoFromData(res, token.processId);
+ const updatedToken = {
+ ...tokenInfo,
+ lastUpdated: new Date().toISOString()
+ };
+
+ if (updatedToken) {
+ const index = updatedTokens.findIndex(
+ (t) => t.processId === token.processId
+ );
+
+ if (index !== -1) {
+ updatedTokens[index] = { ...updatedTokens[index], ...updatedToken };
+ }
+ }
+ }
+ } catch (err) {
+ console.error(`Failed to update token with id ${token.processId}:`, err);
+ }
+ }
+ await ExtensionStorage.set("ao_tokens", updatedTokens);
+};
diff --git a/src/api/background/handlers/alarms/ao-tokens-import/ao-tokens-import-alarm.handler.ts b/src/api/background/handlers/alarms/ao-tokens-import/ao-tokens-import-alarm.handler.ts
new file mode 100644
index 000000000..47d6b6cce
--- /dev/null
+++ b/src/api/background/handlers/alarms/ao-tokens-import/ao-tokens-import-alarm.handler.ts
@@ -0,0 +1,116 @@
+import Arweave from "arweave";
+import type { Alarms } from "webextension-polyfill";
+import {
+ getAoTokens,
+ getAoTokensCache,
+ getAoTokensAutoImportRestrictedIds
+} from "~tokens";
+import { getTokenInfo } from "~tokens/aoTokens/router";
+import {
+ AO_TOKENS,
+ AO_TOKENS_AUTO_IMPORT_RESTRICTED_IDS,
+ AO_TOKENS_IMPORT_TIMESTAMP,
+ gateway,
+ getNoticeTransactions,
+ verifyCollectiblesType
+} from "~tokens/aoTokens/sync";
+import { withRetry } from "~utils/promises/retry";
+import { timeoutPromise } from "~utils/promises/timeout";
+import { ExtensionStorage } from "~utils/storage";
+import { getActiveAddress } from "~wallets";
+
+/**
+ * Import AO Tokens
+ */
+export async function handleAoTokensImportAlarm(alarm: Alarms.Alarm) {
+ if (alarm?.name !== "import_ao_tokens") return;
+
+ try {
+ const activeAddress = await getActiveAddress();
+
+ let [aoTokens, aoTokensCache, removedTokenIds = []] = await Promise.all([
+ getAoTokens(),
+ getAoTokensCache(),
+ getAoTokensAutoImportRestrictedIds()
+ ]);
+
+ let aoTokensIds = new Set(aoTokens.map(({ processId }) => processId));
+ const aoTokensCacheIds = new Set(
+ aoTokensCache.map(({ processId }) => processId)
+ );
+ let tokenIdstoExclude = new Set([...aoTokensIds, ...removedTokenIds]);
+ const walletTokenIds = new Set([...tokenIdstoExclude, ...aoTokensCacheIds]);
+
+ const arweave = new Arweave(gateway);
+ const { processIds } = await getNoticeTransactions(
+ arweave,
+ activeAddress,
+ Array.from(walletTokenIds)
+ );
+
+ const newProcessIds = Array.from(
+ new Set([...processIds, ...aoTokensCacheIds])
+ ).filter((processId) => !tokenIdstoExclude.has(processId));
+
+ if (newProcessIds.length === 0) {
+ console.log("No new ao tokens found!");
+ return;
+ }
+
+ const promises = newProcessIds
+ .filter((processId) => !aoTokensCacheIds.has(processId))
+ .map((processId) =>
+ withRetry(async () => {
+ const token = await timeoutPromise(getTokenInfo(processId), 3000);
+ return { ...token, processId };
+ }, 2)
+ );
+ const results = await Promise.allSettled(promises);
+
+ const tokens = [];
+ const tokensToRestrict = [];
+ results.forEach((result) => {
+ if (result.status === "fulfilled") {
+ const token = result.value;
+ if (token.Ticker) {
+ tokens.push(token);
+ } else if (!removedTokenIds.includes(token.processId)) {
+ tokensToRestrict.push(token);
+ }
+ }
+ });
+
+ const updatedTokens = [...aoTokensCache, ...tokens];
+
+ aoTokens = await getAoTokens();
+ aoTokensIds = new Set(aoTokens.map(({ processId }) => processId));
+ tokenIdstoExclude = new Set([...aoTokensIds, ...removedTokenIds]);
+
+ if (tokensToRestrict.length > 0) {
+ removedTokenIds.push(
+ ...tokensToRestrict.map(({ processId }) => processId)
+ );
+ await ExtensionStorage.set(
+ AO_TOKENS_AUTO_IMPORT_RESTRICTED_IDS,
+ removedTokenIds
+ );
+ }
+
+ let newTokens = updatedTokens.filter(
+ (token) => !tokenIdstoExclude.has(token.processId)
+ );
+ if (newTokens.length === 0) return;
+
+ // Verify collectibles type
+ newTokens = await verifyCollectiblesType(newTokens, arweave);
+
+ newTokens.forEach((token) => aoTokens.push(token));
+ await ExtensionStorage.set(AO_TOKENS, aoTokens);
+
+ console.log("Imported ao tokens!");
+ } catch (error: any) {
+ console.log("Error importing tokens: ", error?.message);
+ } finally {
+ await ExtensionStorage.set(AO_TOKENS_IMPORT_TIMESTAMP, 0);
+ }
+}
diff --git a/src/api/background/handlers/alarms/gateway-update/gateway-update-alarm.handler.ts b/src/api/background/handlers/alarms/gateway-update/gateway-update-alarm.handler.ts
new file mode 100644
index 000000000..d77a217ef
--- /dev/null
+++ b/src/api/background/handlers/alarms/gateway-update/gateway-update-alarm.handler.ts
@@ -0,0 +1,57 @@
+import { extractGarItems, pingUpdater } from "~lib/wayfinder";
+import { type Alarms } from "webextension-polyfill";
+import { AOProcess } from "~lib/ao";
+import { AO_ARNS_PROCESS } from "~lib/arns";
+import type {
+ GatewayAddressRegistryItem,
+ GatewayAddressRegistryItemData,
+ PaginatedResult
+} from "~gateways/types";
+import {
+ RETRY_ALARM,
+ scheduleGatewayUpdate,
+ UPDATE_ALARM,
+ updateGatewayCache
+} from "~gateways/cache";
+
+/**
+ * Gateway cache update call. Usually called by an alarm,
+ * but will also be executed on install.
+ */
+export async function handleGatewayUpdateAlarm(alarm?: Alarms.Alarm) {
+ if (alarm && ![UPDATE_ALARM, RETRY_ALARM].includes(alarm.name)) {
+ return;
+ }
+
+ let garItemsWithChecks: GatewayAddressRegistryItem[] = [];
+
+ try {
+ // fetch cache
+ const ArIO = new AOProcess({ processId: AO_ARNS_PROCESS });
+
+ const gatewaysResult = await ArIO.read<
+ PaginatedResult
+ >({
+ tags: [
+ { name: "Action", value: "Paginated-Gateways" },
+ { name: "Sort-By", value: "operatorStake" },
+ { name: "Sort-Order", value: "desc" },
+ { name: "Limit", value: "100" }
+ ]
+ });
+
+ const garItems = extractGarItems(gatewaysResult.items);
+
+ // healtcheck
+ await pingUpdater(garItems, (nextGarItemsWithChecks) => {
+ garItemsWithChecks = nextGarItemsWithChecks;
+ });
+
+ await updateGatewayCache(garItemsWithChecks);
+ } catch (err) {
+ console.log("err in handle", err);
+
+ // schedule to try again
+ await scheduleGatewayUpdate(true);
+ }
+}
diff --git a/src/api/background/handlers/alarms/key-removal/key-removal-alarm.handler.ts b/src/api/background/handlers/alarms/key-removal/key-removal-alarm.handler.ts
new file mode 100644
index 000000000..a8c836f07
--- /dev/null
+++ b/src/api/background/handlers/alarms/key-removal/key-removal-alarm.handler.ts
@@ -0,0 +1,16 @@
+import type { Alarms } from "webextension-polyfill";
+import { getDecryptionKey, removeDecryptionKey } from "~wallets/auth";
+
+/**
+ * Listener for the key removal alarm
+ */
+export async function handleKeyRemovalAlarm(alarm: Alarms.Alarm) {
+ if (alarm.name !== "remove_decryption_key_scheduled") return;
+
+ // check if there is a decryption key
+ const decryptionKey = await getDecryptionKey();
+ if (!decryptionKey) return;
+
+ // remove the decryption key
+ await removeDecryptionKey();
+}
diff --git a/src/notifications/api.ts b/src/api/background/handlers/alarms/notifications/notifications-alarm.handler.ts
similarity index 59%
rename from src/notifications/api.ts
rename to src/api/background/handlers/alarms/notifications/notifications-alarm.handler.ts
index 81021229b..14713a894 100644
--- a/src/notifications/api.ts
+++ b/src/api/background/handlers/alarms/notifications/notifications-alarm.handler.ts
@@ -2,56 +2,18 @@ import { ExtensionStorage } from "~utils/storage";
import { getActiveAddress } from "~wallets";
import iconUrl from "url:/assets/icon512.png";
import browser, { type Alarms } from "webextension-polyfill";
-import { gql } from "~gateways/api";
-import { suggestedGateways } from "~gateways/gateway";
+import { arNotificationsHandler } from "~api/background/handlers/alarms/notifications/notifications-alarm.utils";
import {
+ ALL_AR_RECEIVER_QUERY,
+ ALL_AR_SENT_QUERY,
AO_RECEIVER_QUERY,
AO_SENT_QUERY,
AR_RECEIVER_QUERY,
- AR_SENT_QUERY,
- ALL_AR_RECEIVER_QUERY,
- ALL_AR_SENT_QUERY,
- combineAndSortTransactions,
- processTransactions
-} from "./utils";
-import BigNumber from "bignumber.js";
-
-export type RawTransaction = {
- node: {
- id: string;
- recipient: string;
- owner: {
- address: string;
- };
- quantity: {
- ar: string;
- };
- fee: {
- ar: string;
- };
- block: {
- timestamp: number;
- height: number;
- };
- tags: Array<{
- name: string;
- value: string;
- }>;
- };
-};
-
-export type Transaction = RawTransaction & {
- transactionType: string;
- quantity: string;
- isAo?: boolean;
- tokenId?: string;
- warpContract?: boolean;
-};
+ AR_SENT_QUERY
+} from "~notifications/utils";
-type ArNotificationsHandlerReturnType = [Transaction[], number, any[]];
-
-export async function notificationsHandler(alarmInfo?: Alarms.Alarm) {
- if (alarmInfo && !alarmInfo.name.startsWith("notifications")) return;
+export async function handleNotificationsAlarm(alarm?: Alarms.Alarm) {
+ if (alarm && !alarm.name.startsWith("notifications")) return;
const notificationSetting: boolean = await ExtensionStorage.get(
"setting_notifications"
@@ -182,65 +144,3 @@ export async function notificationsHandler(alarmInfo?: Alarms.Alarm) {
console.error("Error updating notifications:", err);
}
}
-
-const arNotificationsHandler = async (
- address: string,
- lastStoredHeight: number,
- notificationSetting: boolean,
- queriesConfig: {
- query: string;
- variables: Record;
- isAllTxns?: boolean;
- }[]
-): Promise => {
- try {
- let transactionDiff = [];
-
- const queries = queriesConfig.map((config) =>
- gql(config.query, config.variables, suggestedGateways[1])
- );
- let responses = await Promise.all(queries);
- responses = responses.map((response, index) => {
- if (
- typeof queriesConfig[index].isAllTxns === "boolean" &&
- !queriesConfig[index].isAllTxns
- ) {
- response.data.transactions.edges =
- response.data.transactions.edges.filter((edge) =>
- BigNumber(edge.node.quantity.ar).gt(0)
- );
- }
- return response;
- });
-
- const combinedTransactions = combineAndSortTransactions(responses);
-
- const enrichedTransactions = processTransactions(
- combinedTransactions,
- address
- );
-
- const newMaxHeight = Math.max(
- ...enrichedTransactions
- .filter((tx) => tx.node.block) // Filter out transactions without a block
- .map((tx) => tx.node.block.height)
- );
- // filters out transactions that are older than last stored height,
- if (newMaxHeight !== lastStoredHeight) {
- const newTransactions = enrichedTransactions.filter(
- (transaction) =>
- transaction.node.block &&
- transaction.node.block.height > lastStoredHeight
- );
-
- // if it's the first time loading notifications, don't send a message && notifications are enabled
- if (lastStoredHeight !== 0 && notificationSetting) {
- await ExtensionStorage.set("new_notifications", true);
- transactionDiff = newTransactions;
- }
- }
- return [enrichedTransactions, newMaxHeight, transactionDiff];
- } catch (err) {
- console.log("err", err);
- }
-};
diff --git a/src/api/background/handlers/alarms/notifications/notifications-alarm.utils.ts b/src/api/background/handlers/alarms/notifications/notifications-alarm.utils.ts
new file mode 100644
index 000000000..2362b97d3
--- /dev/null
+++ b/src/api/background/handlers/alarms/notifications/notifications-alarm.utils.ts
@@ -0,0 +1,101 @@
+import BigNumber from "bignumber.js";
+import { gql } from "~gateways/api";
+import { suggestedGateways } from "~gateways/gateway";
+import {
+ combineAndSortTransactions,
+ processTransactions
+} from "~notifications/utils";
+import { ExtensionStorage } from "~utils/storage";
+
+export type RawTransaction = {
+ node: {
+ id: string;
+ recipient: string;
+ owner: {
+ address: string;
+ };
+ quantity: {
+ ar: string;
+ };
+ block: {
+ timestamp: number;
+ height: number;
+ };
+ tags: Array<{
+ name: string;
+ value: string;
+ }>;
+ };
+};
+
+export type Transaction = RawTransaction & {
+ transactionType: string;
+ quantity: string;
+ isAo?: boolean;
+ tokenId?: string;
+ warpContract?: boolean;
+};
+
+type ArNotificationsHandlerReturnType = [Transaction[], number, any[]];
+
+export async function arNotificationsHandler(
+ address: string,
+ lastStoredHeight: number,
+ notificationSetting: boolean,
+ queriesConfig: {
+ query: string;
+ variables: Record;
+ isAllTxns?: boolean;
+ }[]
+): Promise {
+ try {
+ let transactionDiff = [];
+
+ const queries = queriesConfig.map((config) =>
+ gql(config.query, config.variables, suggestedGateways[1])
+ );
+ let responses = await Promise.all(queries);
+ responses = responses.map((response, index) => {
+ if (
+ typeof queriesConfig[index].isAllTxns === "boolean" &&
+ !queriesConfig[index].isAllTxns
+ ) {
+ response.data.transactions.edges =
+ response.data.transactions.edges.filter((edge) =>
+ BigNumber(edge.node.quantity.ar).gt(0)
+ );
+ }
+ return response;
+ });
+
+ const combinedTransactions = combineAndSortTransactions(responses);
+
+ const enrichedTransactions = processTransactions(
+ combinedTransactions,
+ address
+ );
+
+ const newMaxHeight = Math.max(
+ ...enrichedTransactions
+ .filter((tx) => tx.node.block) // Filter out transactions without a block
+ .map((tx) => tx.node.block.height)
+ );
+ // filters out transactions that are older than last stored height,
+ if (newMaxHeight !== lastStoredHeight) {
+ const newTransactions = enrichedTransactions.filter(
+ (transaction) =>
+ transaction.node.block &&
+ transaction.node.block.height > lastStoredHeight
+ );
+
+ // if it's the first time loading notifications, don't send a message && notifications are enabled
+ if (lastStoredHeight !== 0 && notificationSetting) {
+ await ExtensionStorage.set("new_notifications", true);
+ transactionDiff = newTransactions;
+ }
+ }
+ return [enrichedTransactions, newMaxHeight, transactionDiff];
+ } catch (err) {
+ console.log("err", err);
+ }
+}
diff --git a/src/subscriptions/api.ts b/src/api/background/handlers/alarms/subscriptions/subscriptions-alarm.handler.ts
similarity index 81%
rename from src/subscriptions/api.ts
rename to src/api/background/handlers/alarms/subscriptions/subscriptions-alarm.handler.ts
index 2c431a96a..f9855ae9e 100644
--- a/src/subscriptions/api.ts
+++ b/src/api/background/handlers/alarms/subscriptions/subscriptions-alarm.handler.ts
@@ -1,9 +1,8 @@
-import type { SubscriptionData } from "./subscription";
import { addSubscription, getSubscriptionData } from "~subscriptions";
-import { ExtensionStorage } from "~utils/storage";
import { getActiveAddress } from "~wallets";
-import { handleSubscriptionPayment } from "./payments";
import type { Alarms } from "webextension-polyfill";
+import { handleSubscriptionPayment } from "~subscriptions/payments";
+import type { SubscriptionData } from "~subscriptions/subscription";
/**
* + fetch subscription auto withdrawal allowance
@@ -13,7 +12,7 @@ import type { Alarms } from "webextension-polyfill";
* + notify user of manual payments
*/
-export async function subscriptionsHandler(alarmInfo?: Alarms.Alarm) {
+export async function handleSubscriptionsAlarm(alarmInfo?: Alarms.Alarm) {
if (alarmInfo && !alarmInfo.name.startsWith("subscription-alarm-")) return;
const prefixLength = "subscription-alarm-".length;
diff --git a/src/api/background/handlers/alarms/sync-labels/sync-labels-alarm.handler.ts b/src/api/background/handlers/alarms/sync-labels/sync-labels-alarm.handler.ts
new file mode 100644
index 000000000..c593ea466
--- /dev/null
+++ b/src/api/background/handlers/alarms/sync-labels/sync-labels-alarm.handler.ts
@@ -0,0 +1,38 @@
+import { getAnsProfile, type AnsUser } from "~lib/ans";
+import type { Alarms } from "webextension-polyfill";
+import { ExtensionStorage } from "~utils/storage";
+import { getWallets } from "~wallets";
+
+/**
+ * Sync nicknames with ANS labels
+ */
+export async function handleSyncLabelsAlarm(alarm?: Alarms.Alarm) {
+ // check alarm name if called from an alarm
+ if (alarm && alarm.name !== "sync_labels") {
+ return;
+ }
+
+ // get wallets
+ const wallets = await getWallets();
+
+ if (wallets.length === 0) return;
+
+ // get profiles
+ const profiles = (await getAnsProfile(
+ wallets.map((w) => w.address)
+ )) as AnsUser[];
+
+ const find = (addr: string) =>
+ profiles.find((w) => w.user === addr)?.currentLabel;
+
+ // save updated wallets
+ await ExtensionStorage.set(
+ "wallets",
+ wallets.map((wallet) => ({
+ ...wallet,
+ nickname: find(wallet.address)
+ ? find(wallet.address) + ".ar"
+ : wallet.nickname
+ }))
+ );
+}
diff --git a/src/api/background/handlers/alarms/track-balance/track-balance-alarm.handler.ts b/src/api/background/handlers/alarms/track-balance/track-balance-alarm.handler.ts
new file mode 100644
index 000000000..9c4fcbba9
--- /dev/null
+++ b/src/api/background/handlers/alarms/track-balance/track-balance-alarm.handler.ts
@@ -0,0 +1,46 @@
+import { getWallets } from "~wallets";
+import Arweave from "arweave";
+import { defaultGateway } from "~gateways/gateway";
+import browser, { type Alarms } from "webextension-polyfill";
+import BigNumber from "bignumber.js";
+import {
+ EventType,
+ setToStartOfNextMonth,
+ trackDirect
+} from "~utils/analytics";
+
+export async function handleTrackBalanceAlarm(alarmInfo?: Alarms.Alarm) {
+ if (alarmInfo && !alarmInfo.name.startsWith("track-balance")) return;
+
+ const wallets = await getWallets();
+ const arweave = new Arweave(defaultGateway);
+
+ let totalBalance = BigNumber("0");
+
+ await Promise.all(
+ wallets.map(async ({ address }) => {
+ try {
+ const balance = arweave.ar.winstonToAr(
+ await arweave.wallets.getBalance(address)
+ );
+ totalBalance = totalBalance.plus(balance);
+ } catch (e) {
+ console.log("invalid", e);
+ }
+ })
+ );
+
+ try {
+ await trackDirect(EventType.BALANCE, {
+ totalBalance: totalBalance.toFixed()
+ });
+
+ const timer = setToStartOfNextMonth(new Date());
+
+ browser.alarms.create("track-balance", {
+ when: timer.getTime()
+ });
+ } catch (err) {
+ console.log("err tracking", err);
+ }
+}
diff --git a/src/api/background/handlers/browser/install/install.handler.ts b/src/api/background/handlers/browser/install/install.handler.ts
new file mode 100644
index 000000000..c7a39a879
--- /dev/null
+++ b/src/api/background/handlers/browser/install/install.handler.ts
@@ -0,0 +1,41 @@
+import { scheduleGatewayUpdate } from "~gateways/cache";
+import browser, { type Runtime } from "webextension-polyfill";
+import { loadTokens } from "~tokens/token";
+import { initializeARBalanceMonitor } from "~utils/analytics";
+import { updateAoToken } from "~utils/ao_import";
+import { handleGatewayUpdateAlarm } from "~api/background/handlers/alarms/gateway-update/gateway-update-alarm.handler";
+import { openOrSelectWelcomePage } from "~wallets";
+
+/**
+ * On extension installed event handler
+ */
+export async function handleInstall(details: Runtime.OnInstalledDetailsType) {
+ // only run on install
+ if (details.reason === "install") {
+ openOrSelectWelcomePage(true);
+ }
+
+ // init monthly AR
+ await initializeARBalanceMonitor();
+
+ // initialize alarm to fetch notifications
+ browser.alarms.create("notifications", { periodInMinutes: 1 });
+
+ // reset notifications
+ // await ExtensionStorage.set("show_announcement", true);
+
+ // initialize alarm to update tokens once a week
+ browser.alarms.create("update_ao_tokens", {
+ periodInMinutes: 10080
+ });
+
+ // initialize tokens in wallet
+ await loadTokens();
+
+ // update old ao token to new ao token
+ await updateAoToken();
+
+ // wayfinder
+ await scheduleGatewayUpdate();
+ await handleGatewayUpdateAlarm();
+}
diff --git a/src/api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler.ts b/src/api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler.ts
new file mode 100644
index 000000000..0c2fc890b
--- /dev/null
+++ b/src/api/background/handlers/browser/printer/get-capabilities/get-capabilities.handler.ts
@@ -0,0 +1,75 @@
+import { ARCONNECT_PRINTER_ID } from "~api/background/handlers/browser/printer/printer.constants";
+
+/**
+ * Printer capabilities request callback type
+ */
+type PrinterInfoCallback = (
+ capabilities: chrome.printerProvider.PrinterCapabilities
+) => void;
+
+/**
+ * Tells Chrome about the virtual printer's
+ * capabilities in CDD format
+ */
+export function handleGetCapabilities(
+ printerId: string,
+ callback: PrinterInfoCallback
+) {
+ // only return capabilities for the ArConnect printer
+ if (printerId !== ARCONNECT_PRINTER_ID) return;
+
+ // mimic a regular printer's capabilities
+ callback({
+ capabilities: {
+ version: "1.0",
+ printer: {
+ supported_content_type: [
+ { content_type: "application/pdf" },
+ { content_type: "image/pwg-raster" }
+ ],
+ color: {
+ option: [
+ { type: "STANDARD_COLOR", is_default: true },
+ { type: "STANDARD_MONOCHROME" }
+ ]
+ },
+ copies: {
+ default_copies: 1,
+ max_copies: 100
+ },
+ media_size: {
+ option: [
+ {
+ name: "ISO_A4",
+ width_microns: 210000,
+ height_microns: 297000,
+ is_default: true
+ },
+ {
+ name: "NA_LETTER",
+ width_microns: 215900,
+ height_microns: 279400
+ }
+ ]
+ },
+ page_orientation: {
+ option: [
+ {
+ type: "PORTRAIT",
+ is_default: true
+ },
+ { type: "LANDSCAPE" },
+ { type: "AUTO" }
+ ]
+ },
+ duplex: {
+ option: [
+ { type: "NO_DUPLEX", is_default: true },
+ { type: "LONG_EDGE" },
+ { type: "SHORT_EDGE" }
+ ]
+ }
+ }
+ }
+ });
+}
diff --git a/src/api/background/handlers/browser/printer/get-printers/get-printers.handler.ts b/src/api/background/handlers/browser/printer/get-printers/get-printers.handler.ts
new file mode 100644
index 000000000..6e7906db6
--- /dev/null
+++ b/src/api/background/handlers/browser/printer/get-printers/get-printers.handler.ts
@@ -0,0 +1,22 @@
+import { ARCONNECT_PRINTER_ID } from "~api/background/handlers/browser/printer/printer.constants";
+
+/**
+ * Printer info request callback type
+ */
+type PrinterInfoCallback = (p: chrome.printerProvider.PrinterInfo[]) => void;
+
+/**
+ * Returns a list of "virtual" printers,
+ * in our case "Print/Publish to Arweave"
+ */
+export function handleGetPrinters(callback: PrinterInfoCallback) {
+ callback([
+ {
+ id: ARCONNECT_PRINTER_ID,
+ // TODO: Add to i18n:
+ name: "Print to Arweave",
+ description:
+ "Publish the content you want to print on Arweave, permanently."
+ }
+ ]);
+}
diff --git a/src/lib/printer.ts b/src/api/background/handlers/browser/printer/print/print.handler.ts
similarity index 64%
rename from src/lib/printer.ts
rename to src/api/background/handlers/browser/printer/print/print.handler.ts
index 330fb0401..34c0577ae 100644
--- a/src/lib/printer.ts
+++ b/src/api/background/handlers/browser/printer/print/print.handler.ts
@@ -1,3 +1,4 @@
+import { ARCONNECT_PRINTER_ID } from "~api/background/handlers/browser/printer/printer.constants";
import { uploadDataToTurbo } from "~api/modules/dispatch/uploader";
import { getActiveKeyfile, type DecryptedWallet } from "~wallets";
import { freeDecryptedWallet } from "~wallets/encryption";
@@ -8,104 +9,17 @@ import browser from "webextension-polyfill";
import Arweave from "arweave";
import { signAuth } from "~api/modules/sign/sign_auth";
import { getActiveTab } from "~applications";
-import { sleep } from "~utils/sleep";
-
-const ARCONNECT_PRINTER_ID = "arconnect-permaweb-printer";
-
-/**
- * Tells Chrome about the virtual printer's
- * capabilities in CDD format
- */
-export function getCapabilities(
- printerId: string,
- callback: PrinterCapabilitiesCallback
-) {
- // only return capabilities for the ArConnect printer
- if (printerId !== ARCONNECT_PRINTER_ID) return;
-
- // mimic a regular printer's capabilities
- callback({
- version: "1.0",
- printer: {
- supported_content_type: [
- { content_type: "application/pdf" },
- { content_type: "image/pwg-raster" }
- ],
- color: {
- option: [
- { type: "STANDARD_COLOR", is_default: true },
- { type: "STANDARD_MONOCHROME" }
- ]
- },
- copies: {
- default_copies: 1,
- max_copies: 100
- },
- media_size: {
- option: [
- {
- name: "ISO_A4",
- width_microns: 210000,
- height_microns: 297000,
- is_default: true
- },
- {
- name: "NA_LETTER",
- width_microns: 215900,
- height_microns: 279400
- }
- ]
- },
- page_orientation: {
- option: [
- {
- type: "PORTRAIT",
- is_default: true
- },
- { type: "LANDSCAPE" },
- { type: "AUTO" }
- ]
- },
- duplex: {
- option: [
- { type: "NO_DUPLEX", is_default: true },
- { type: "LONG_EDGE" },
- { type: "SHORT_EDGE" }
- ]
- }
- }
- });
-}
-
-/**
- * Printer capabilities request callback type
- */
-type PrinterCapabilitiesCallback = (p: unknown) => void;
+import { sleep } from "~utils/promises/sleep";
/**
- * Returns a list of "virtual" printers,
- * in our case "Print/Publish to Arweave"
- */
-export function getPrinters(callback: PrinterInfoCallback) {
- callback([
- {
- id: ARCONNECT_PRINTER_ID,
- name: "Print to Arweave",
- description:
- "Publish the content you want to print on Arweave, permanently."
- }
- ]);
-}
-
-/**
- * Printer info request callback type
+ * Print request (result) callback
*/
-type PrinterInfoCallback = (p: chrome.printerProvider.PrinterInfo[]) => void;
+type PrintCallback = (result: string) => void;
/**
* Handles the request from the user to print the page to Arweave
*/
-export async function handlePrintRequest(
+export async function handlePrint(
printJob: chrome.printerProvider.PrintJob,
resultCallback: PrintCallback
) {
@@ -158,7 +72,10 @@ export async function handlePrintRequest(
const activeTab = await getActiveTab();
await signAuth(
- activeTab.url,
+ {
+ tabID: activeTab.id,
+ url: activeTab.url
+ },
// @ts-expect-error
{
...dataEntry.toJSON(),
@@ -220,8 +137,3 @@ export async function handlePrintRequest(
if (decryptedWallet?.type == "local")
freeDecryptedWallet(decryptedWallet.keyfile);
}
-
-/**
- * Print request (result) callback
- */
-type PrintCallback = (result: string) => void;
diff --git a/src/api/background/handlers/browser/printer/printer.constants.ts b/src/api/background/handlers/browser/printer/printer.constants.ts
new file mode 100644
index 000000000..831c7fbac
--- /dev/null
+++ b/src/api/background/handlers/browser/printer/printer.constants.ts
@@ -0,0 +1 @@
+export const ARCONNECT_PRINTER_ID = "arconnect-permaweb-printer";
diff --git a/src/api/background/handlers/browser/protocol/protocol.handler.ts b/src/api/background/handlers/browser/protocol/protocol.handler.ts
new file mode 100644
index 000000000..2b7b886cf
--- /dev/null
+++ b/src/api/background/handlers/browser/protocol/protocol.handler.ts
@@ -0,0 +1,33 @@
+import { getRedirectURL } from "~gateways/ar_protocol";
+import { findGateway } from "~gateways/wayfinder";
+import browser, { type WebNavigation } from "webextension-polyfill";
+
+/**
+ * Handle custom ar:// protocol, using the
+ * browser.webNavigation.onBeforeNavigate API.
+ *
+ * This is based on the issue in ipfs/ipfs-companion:
+ * https://github.com/ipfs/ipfs-companion/issues/164#issuecomment-328374052
+ *
+ * Thank you ar.io for the updated method:
+ * https://github.com/ar-io/wayfinder/blob/main/background.js#L13
+ */
+export async function handleProtocol(
+ details: WebNavigation.OnBeforeNavigateDetailsType
+) {
+ const gateway = await findGateway({
+ arns: true,
+ ensureStake: true
+ });
+
+ // parse redirect url
+ const redirectUrl = getRedirectURL(new URL(details.url), gateway);
+
+ // don't do anything if it is not a protocol call
+ if (!redirectUrl) return;
+
+ // update tab
+ browser.tabs.update(details.tabId, {
+ url: redirectUrl
+ });
+}
diff --git a/src/api/background/handlers/browser/tabs/tabs.handler.ts b/src/api/background/handlers/browser/tabs/tabs.handler.ts
new file mode 100644
index 000000000..ffff25174
--- /dev/null
+++ b/src/api/background/handlers/browser/tabs/tabs.handler.ts
@@ -0,0 +1,89 @@
+import Application from "~applications/application";
+import { getTab } from "~applications/tab";
+import {
+ getCachedAuthPopupWindowTabID,
+ resetKeepAlive,
+ resetPopupTabID
+} from "~utils/auth/auth.utils";
+import { createContextMenus } from "~utils/context_menus";
+import { getAppURL } from "~utils/format";
+import { updateIcon } from "~utils/icon";
+import { isomorphicSendMessage } from "~utils/messaging/messaging.utils";
+import browser from "webextension-polyfill";
+
+/**
+ * Handle tab updates (icon change, context menus, etc.)
+ *
+ * @param tabId ID of the tab to get.
+ */
+export async function handleTabUpdate(
+ tabID: number,
+ changeInfo?: browser.Tabs.OnUpdatedChangeInfoType
+) {
+ const popupTabID = getCachedAuthPopupWindowTabID();
+
+ if (popupTabID !== -1 && changeInfo?.status === "loading") {
+ isomorphicSendMessage({
+ messageId: "auth_tab_reloaded",
+ tabId: popupTabID,
+ data: tabID
+ });
+ }
+
+ // construct app
+ const tab = await getTab(tabID);
+
+ // if we cannot parse the tab URL, the extension is not connected
+ if (!tab?.url) {
+ updateIcon(false);
+ createContextMenus(false);
+ return;
+ }
+
+ const app = new Application(getAppURL(tab.url));
+
+ // change icon to "connected" status if
+ // the site is connected and add the
+ // context menus
+ const connected = await app.isConnected();
+
+ updateIcon(connected);
+ createContextMenus(connected);
+}
+
+/**
+ * Notifies the auth popup about closed tab for it to abort AuthRequests coming from those tabs.
+ *
+ * @param tabId ID of the closed tab.
+ */
+export async function handleTabClosed(closedTabID: number) {
+ const popupTabID = getCachedAuthPopupWindowTabID();
+
+ // If there's no popup, then we do nothing:
+ if (popupTabID === -1) return;
+
+ if (closedTabID === popupTabID) {
+ // If the closed tab was the popup, we reset its ID and the keep-alive alarm:
+ resetPopupTabID();
+ resetKeepAlive();
+
+ // Make sure there were no duplicate auth popups and, if so, close them too:
+ try {
+ const url = browser.runtime.getURL("tabs/auth.html");
+ const authPopups = await browser.tabs.query({ url });
+
+ browser.tabs.remove(authPopups.map((authPopup) => authPopup.id));
+ } catch (err) {
+ console.warn("Error trying to close other auth popups:", err);
+ }
+
+ return;
+ }
+
+ // If some other tab was closed and there's a popup, notify the popup in case it has AuthRequest from the closed tab:
+ isomorphicSendMessage({
+ messageId: "auth_tab_closed",
+ tabId: popupTabID,
+ data: closedTabID
+ });
+}
diff --git a/src/api/background/handlers/browser/window-close/window-close.handler.ts b/src/api/background/handlers/browser/window-close/window-close.handler.ts
new file mode 100644
index 000000000..a9b2d1a5f
--- /dev/null
+++ b/src/api/background/handlers/browser/window-close/window-close.handler.ts
@@ -0,0 +1,20 @@
+import { removeDecryptionKey } from "~wallets/auth";
+import browser from "webextension-polyfill";
+
+/**
+ * Listener for browser close.
+ * On browser closed, we remove the
+ * decryptionKey.
+ */
+export async function handleWindowClose() {
+ const windows = await browser.windows.getAll();
+
+ // TODO: Maybe we should be counting connected apps instead?
+ // return if there are still windows open
+ if (windows.length > 0) {
+ return;
+ }
+
+ // remove the decryption key
+ await removeDecryptionKey();
+}
diff --git a/src/api/index.ts b/src/api/background/handlers/message/api-call-message/api-call-message.handler.ts
similarity index 62%
rename from src/api/index.ts
rename to src/api/background/handlers/message/api-call-message/api-call-message.handler.ts
index 05d2fe800..5e3a5b75e 100644
--- a/src/api/index.ts
+++ b/src/api/background/handlers/message/api-call-message/api-call-message.handler.ts
@@ -1,16 +1,18 @@
import { isExactly, isNotUndefined, isString } from "typed-assert";
import type { OnMessageCallback } from "@arconnect/webext-bridge";
-import { type Chunk, handleChunk } from "./modules/sign/chunks";
-import { isApiCall, isChunk } from "~utils/assertions";
+import { isApiCall } from "~utils/assertions";
import Application from "~applications/application";
import type { ApiCall, ApiResponse } from "shim";
import browser from "webextension-polyfill";
import { getTab } from "~applications/tab";
import { pushEvent } from "~utils/events";
import { getAppURL } from "~utils/format";
-import modules from "./background";
+import {
+ backgroundModules,
+ type ModuleAppData
+} from "~api/background/background-modules";
-export const handleApiCalls: OnMessageCallback<
+export const handleApiCallMessage: OnMessageCallback<
// @ts-expect-error
ApiCall,
ApiResponse
@@ -45,7 +47,9 @@ export const handleApiCalls: OnMessageCallback<
// find module to execute
const functionName = data.type.replace("api_", "");
- const mod = modules.find((mod) => mod.functionName === functionName);
+ const mod = backgroundModules.find(
+ (mod) => mod.functionName === functionName
+ );
// if we cannot find the module, we return with an error
isNotUndefined(mod, `API function "${functionName}" not found`);
@@ -97,9 +101,10 @@ export const handleApiCalls: OnMessageCallback<
// handle function
const functionResult = await mod.function(
{
- appURL: app.url,
+ tabID: tab.id,
+ url: app.url,
favicon: tab.favIconUrl
- },
+ } satisfies ModuleAppData,
...(data.data.params || [])
);
@@ -119,65 +124,3 @@ export const handleApiCalls: OnMessageCallback<
};
}
};
-
-export const handleChunkCalls: OnMessageCallback<
- // @ts-expect-error
- ApiCall,
- ApiResponse
-> = async ({ data, sender }) => {
- // construct base message to extend and return
- const baseMessage: ApiResponse = {
- type: "chunk_result",
- callID: data.callID
- };
-
- try {
- // validate message
- isExactly(
- sender.context,
- "content-script",
- "Chunk calls are only accepted from the injected-script -> content-script"
- );
- isChunk(data.data);
-
- // grab the tab where the chunk came
- const tab = await getTab(sender.tabId);
-
- // if the tab is not found, reject the call
- isString(tab?.url, "Call coming from invalid tab");
-
- // raw url where the chunk originates from
- let url = tab.url;
-
- // if the frame ID is defined, the API
- // request is not coming from the main tab
- // but from an iframe in the tab.
- // we need to treat the iframe as a separate
- // application to ensure the user does not
- // mistake it for the actual app
- if (typeof sender.frameId !== "undefined") {
- const frame = await browser.webNavigation.getFrame({
- frameId: sender.frameId,
- tabId: sender.tabId
- });
-
- // update url value with the url belonging to the frame
- if (frame.url) url = frame.url;
- }
-
- // call the chunk handler
- const index = handleChunk(data.data, getAppURL(url));
-
- return {
- ...baseMessage,
- data: index
- };
- } catch (e) {
- // return error
- return {
- ...baseMessage,
- error: true,
- data: e?.message || e
- };
- }
-};
diff --git a/src/api/background/handlers/message/chunk-message/chunk-message.handler.ts b/src/api/background/handlers/message/chunk-message/chunk-message.handler.ts
new file mode 100644
index 000000000..5bd19ca60
--- /dev/null
+++ b/src/api/background/handlers/message/chunk-message/chunk-message.handler.ts
@@ -0,0 +1,70 @@
+import { isExactly, isString } from "typed-assert";
+import type { OnMessageCallback } from "@arconnect/webext-bridge";
+import { type Chunk, handleChunk } from "../../../../modules/sign/chunks";
+import { isChunk } from "~utils/assertions";
+import type { ApiCall, ApiResponse } from "shim";
+import browser from "webextension-polyfill";
+import { getTab } from "~applications/tab";
+import { getAppURL } from "~utils/format";
+
+export const handleChunkMessage: OnMessageCallback<
+ // @ts-expect-error
+ ApiCall,
+ ApiResponse
+> = async ({ data, sender }) => {
+ // construct base message to extend and return
+ const baseMessage: ApiResponse = {
+ type: "chunk_result",
+ callID: data.callID
+ };
+
+ try {
+ // validate message
+ isExactly(
+ sender.context,
+ "content-script",
+ "Chunk calls are only accepted from the injected-script -> content-script"
+ );
+ isChunk(data.data);
+
+ // grab the tab where the chunk came
+ const tab = await getTab(sender.tabId);
+
+ // if the tab is not found, reject the call
+ isString(tab?.url, "Call coming from invalid tab");
+
+ // raw url where the chunk originates from
+ let url = tab.url;
+
+ // if the frame ID is defined, the API
+ // request is not coming from the main tab
+ // but from an iframe in the tab.
+ // we need to treat the iframe as a separate
+ // application to ensure the user does not
+ // mistake it for the actual app
+ if (typeof sender.frameId !== "undefined") {
+ const frame = await browser.webNavigation.getFrame({
+ frameId: sender.frameId,
+ tabId: sender.tabId
+ });
+
+ // update url value with the url belonging to the frame
+ if (frame.url) url = frame.url;
+ }
+
+ // call the chunk handler
+ const index = handleChunk(data.data, getAppURL(url));
+
+ return {
+ ...baseMessage,
+ data: index
+ };
+ } catch (e) {
+ // return error
+ return {
+ ...baseMessage,
+ error: true,
+ data: e?.message || e
+ };
+ }
+};
diff --git a/src/api/background/handlers/storage/active-address-change/active-address-change.handler.ts b/src/api/background/handlers/storage/active-address-change/active-address-change.handler.ts
new file mode 100644
index 000000000..64d740aa2
--- /dev/null
+++ b/src/api/background/handlers/storage/active-address-change/active-address-change.handler.ts
@@ -0,0 +1,62 @@
+import { sendMessage } from "@arconnect/webext-bridge";
+import type { StorageChange } from "~utils/runtime";
+import Application from "~applications/application";
+import { forEachTab } from "~applications/tab";
+import { getAppURL } from "~utils/format";
+import { isomorphicSendMessage } from "~utils/messaging/messaging.utils";
+import { getCachedAuthPopupWindowTabID } from "~utils/auth/auth.utils";
+
+/**
+ * Active address change event listener.
+ * Sends a message to fire the "walletSwitch"
+ * event in the tab.
+ */
+export async function handleActiveAddressChange({
+ oldValue,
+ newValue: newAddress
+}: StorageChange) {
+ if (!newAddress || oldValue === newAddress) return;
+
+ // go through all tabs and check if they
+ // have the permissions to receive the
+ // wallet switch event
+ await forEachTab(async (tab) => {
+ const app = new Application(getAppURL(tab.url));
+
+ // check required permissions
+ const permissionCheck = await app.hasPermissions([
+ "ACCESS_ALL_ADDRESSES",
+ "ACCESS_ADDRESS"
+ ]);
+
+ // app not connected
+ if (permissionCheck.has.length === 0) return;
+
+ // trigger emitter
+ await sendMessage(
+ "event",
+ {
+ name: "activeAddress",
+ value: permissionCheck.result ? newAddress : null
+ },
+ `content-script@${tab.id}`
+ );
+
+ const popupTabID = getCachedAuthPopupWindowTabID();
+
+ if (popupTabID) {
+ isomorphicSendMessage({
+ messageId: "auth_active_wallet_change",
+ tabId: popupTabID,
+ data: tab.id
+ });
+ }
+
+ // trigger event via message
+ await sendMessage(
+ "switch_wallet_event",
+ permissionCheck ? newAddress : null,
+ `content-script@${tab.id}`
+ );
+ });
+}
diff --git a/src/applications/events.ts b/src/api/background/handlers/storage/app-config-change/app-config-change.handler.ts
similarity index 87%
rename from src/applications/events.ts
rename to src/api/background/handlers/storage/app-config-change/app-config-change.handler.ts
index b46986f9a..ca7e04f56 100644
--- a/src/applications/events.ts
+++ b/src/api/background/handlers/storage/app-config-change/app-config-change.handler.ts
@@ -1,14 +1,15 @@
-import Application, { type InitAppParams, PREFIX } from "./application";
import { sendMessage } from "@arconnect/webext-bridge";
-import { getMissingPermissions } from "./permissions";
import type { StorageChange } from "~utils/runtime";
import { getStoredApps } from "~applications";
-import { compareGateways } from "../gateways/utils";
import { getAppURL } from "~utils/format";
-import { forEachTab } from "./tab";
import type { Event } from "shim";
+import { getMissingPermissions } from "~applications/permissions";
+import { forEachTab } from "~applications/tab";
+import { compareGateways } from "~gateways/utils";
+import type { InitAppParams } from "~applications/application";
+import Application, { PREFIX } from "~applications/application";
-export async function appConfigChangeListener(
+export async function handleAppConfigChange(
changes: Record>,
areaName: string
) {
@@ -47,7 +48,7 @@ export async function appConfigChangeListener(
storedNewValue as unknown as string
) as InitAppParams;
- // check if permission event emiting is needed
+ // check if permission event emitting is needed
// get missing permissions
const missingPermissions = getMissingPermissions(
oldValue?.permissions || [],
diff --git a/src/api/background/handlers/storage/apps-change/app-change.handler.ts b/src/api/background/handlers/storage/apps-change/app-change.handler.ts
new file mode 100644
index 000000000..c5f8a8926
--- /dev/null
+++ b/src/api/background/handlers/storage/apps-change/app-change.handler.ts
@@ -0,0 +1,93 @@
+import { createContextMenus } from "~utils/context_menus";
+import { sendMessage } from "@arconnect/webext-bridge";
+import type { StorageChange } from "~utils/runtime";
+import { getAppURL } from "~utils/format";
+import { updateIcon } from "~utils/icon";
+import { forEachTab } from "~applications/tab";
+import { getActiveTab } from "~applications";
+import Application from "~applications/application";
+import { isomorphicSendMessage } from "~utils/messaging/messaging.utils";
+import { getCachedAuthPopupWindowTabID } from "~utils/auth/auth.utils";
+
+/**
+ * App disconnected listener. Sends a message
+ * to trigger the disconnected event.
+ */
+export async function handleAppsChange({
+ oldValue,
+ newValue
+}: StorageChange) {
+ // message to send the event
+ const triggerEvent = (tabID: number, type: "connect" | "disconnect") =>
+ sendMessage(
+ "event",
+ {
+ name: type,
+ value: null
+ },
+ `content-script@${tabID}`
+ );
+
+ // trigger events
+ forEachTab(async (tab) => {
+ // get app url
+ const appURL = getAppURL(tab.url);
+
+ // if the new value is undefined
+ // and the old value was defined
+ // we need to emit the disconnect
+ // event for all tabs that were
+ // connected
+ if (!newValue && !!oldValue) {
+ if (!oldValue.includes(appURL)) return;
+
+ const popupTabID = getCachedAuthPopupWindowTabID();
+
+ if (popupTabID) {
+ isomorphicSendMessage({
+ messageId: "auth_app_disconnected",
+ tabId: popupTabID,
+ data: tab.id
+ });
+ }
+
+ return await triggerEvent(tab.id, "disconnect");
+ } else if (!newValue) {
+ // if the new value is undefined
+ // and the old value was also
+ // undefined, we just return
+ return;
+ }
+
+ const oldAppsList = oldValue || [];
+
+ // if the new value includes the app url
+ // and the old value does not, than the
+ // app has just been connected
+ // if the reverse is true, than the app
+ // has just been disconnected
+ if (newValue.includes(appURL) && !oldAppsList.includes(appURL)) {
+ await triggerEvent(tab.id, "connect");
+ } else if (!newValue.includes(appURL) && oldAppsList.includes(appURL)) {
+ const popupTabID = getCachedAuthPopupWindowTabID();
+
+ if (popupTabID) {
+ isomorphicSendMessage({
+ messageId: "auth_app_disconnected",
+ tabId: popupTabID,
+ data: tab.id
+ });
+ }
+
+ await triggerEvent(tab.id, "disconnect");
+ }
+ });
+
+ // update icon and context menus
+ const activeTab = await getActiveTab();
+ const app = new Application(getAppURL(activeTab.url));
+ const connected = await app.isConnected();
+
+ await updateIcon(connected);
+ await createContextMenus(connected);
+}
diff --git a/src/wallets/event.ts b/src/api/background/handlers/storage/wallet-change/wallet-change.handler.ts
similarity index 64%
rename from src/wallets/event.ts
rename to src/api/background/handlers/storage/wallet-change/wallet-change.handler.ts
index a1ae279f4..b1129d626 100644
--- a/src/wallets/event.ts
+++ b/src/api/background/handlers/storage/wallet-change/wallet-change.handler.ts
@@ -6,57 +6,12 @@ import { forEachTab } from "~applications/tab";
import { getAppURL } from "~utils/format";
import browser from "webextension-polyfill";
-/**
- * Active address change event listener.
- * Sends a message to fire the "walletSwitch"
- * event in the tab.
- */
-export async function addressChangeListener({
- oldValue,
- newValue: newAddress
-}: StorageChange) {
- if (!newAddress || oldValue === newAddress) return;
-
- // go through all tabs and check if they
- // have the permissions to receive the
- // wallet switch event
- await forEachTab(async (tab) => {
- const app = new Application(getAppURL(tab.url));
-
- // check required permissions
- const permissionCheck = await app.hasPermissions([
- "ACCESS_ALL_ADDRESSES",
- "ACCESS_ADDRESS"
- ]);
-
- // app not connected
- if (permissionCheck.has.length === 0) return;
-
- // trigger emiter
- await sendMessage(
- "event",
- {
- name: "activeAddress",
- value: permissionCheck.result ? newAddress : null
- },
- `content-script@${tab.id}`
- );
-
- // trigger event via message
- await sendMessage(
- "switch_wallet_event",
- permissionCheck ? newAddress : null,
- `content-script@${tab.id}`
- );
- });
-}
-
/**
* Added wallets change listener.
* Fixup active address in case the current
* active address' wallet has been removed.
*/
-export async function walletsChangeListener({
+export async function handleWalletsChange({
newValue,
oldValue
}: StorageChange) {
diff --git a/src/api/foreground.ts b/src/api/foreground.ts
deleted file mode 100644
index bd079a7b7..000000000
--- a/src/api/foreground.ts
+++ /dev/null
@@ -1,139 +0,0 @@
-import type { Module, ModuleFunction } from "./module";
-
-// import modules
-import permissionsModule from "./modules/permissions";
-import permissions from "./modules/permissions/permissions.foreground";
-import activeAddressModule from "./modules/active_address";
-import activeAddress from "./modules/active_address/active_address.foreground";
-import allAddressesModule from "./modules/all_addresses";
-import allAddresses from "./modules/all_addresses/all_addresses.foreground";
-import publicKeyModule from "./modules/public_key";
-import publicKey from "./modules/public_key/public_key.foreground";
-import walletNamesModule from "./modules/wallet_names";
-import walletNames from "./modules/wallet_names/wallet_names.foreground";
-import arweaveConfigModule from "./modules/arweave_config";
-import arweaveConfig from "./modules/arweave_config/arweave_config.foreground";
-import disconnectModule from "./modules/disconnect";
-import disconnect, {
- finalizer as disconnectFinalizer
-} from "./modules/disconnect/disconnect.foreground";
-import addTokenModule from "./modules/add_token";
-import addToken from "./modules/add_token/add_token.foreground";
-import isTokenAddedModule from "./modules/is_token_added";
-import isTokenAdded from "./modules/is_token_added/is_token_added.foreground";
-import connectModule from "./modules/connect";
-import connect from "./modules/connect/connect.foreground";
-import signModule from "./modules/sign";
-import sign, {
- finalizer as signFinalizer
-} from "./modules/sign/sign.foreground";
-import dispatchModule from "./modules/dispatch";
-import dispatch, {
- finalizer as dispatchFinalizer
-} from "./modules/dispatch/dispatch.foreground";
-import encryptModule from "./modules/encrypt";
-import encrypt, {
- finalizer as encryptFinalizer
-} from "./modules/encrypt/encrypt.foreground";
-import decryptModule from "./modules/decrypt";
-import decrypt, {
- finalizer as decryptFinalizer
-} from "./modules/decrypt/decrypt.foreground";
-import signatureModule from "./modules/signature";
-import signature, {
- finalizer as signatureFinalizer
-} from "./modules/signature/signature.foreground";
-import signMessageModule from "./modules/sign_message";
-import signMessage, {
- finalizer as signMessageFinalizer
-} from "./modules/sign_message/sign_message.foreground";
-import subscriptionModule from "./modules/subscription";
-import subscription from "./modules/subscription/subscription.foreground";
-import privateHashModule from "./modules/private_hash";
-import privateHash, {
- finalizer as privateHashFinalizer
-} from "./modules/private_hash/private_hash.foreground";
-import verifyMessageModule from "./modules/verify_message";
-import verifyMessage from "./modules/verify_message/verify_message.foreground";
-import batchSignDataItemModule from "./modules/batch_sign_data_item";
-import batchSignDataItem, {
- finalizer as batchSignDataItemFinalizer
-} from "./modules/batch_sign_data_item/batch_sign_data_item.foreground";
-import signDataItemModule from "./modules/sign_data_item";
-import signDataItem, {
- finalizer as signDataItemFinalizer
-} from "./modules/sign_data_item/sign_data_item.foreground";
-import userTokensModule from "./modules/user_tokens";
-import userTokens from "./modules/user_tokens/user_tokens.foreground";
-import tokenBalanceModule from "./modules/token_balance";
-import tokenBalance from "./modules/token_balance/token_balance.foreground";
-
-/** Foreground modules */
-const modules: ForegroundModule[] = [
- { ...permissionsModule, function: permissions },
- { ...activeAddressModule, function: activeAddress },
- { ...allAddressesModule, function: allAddresses },
- { ...publicKeyModule, function: publicKey },
- { ...walletNamesModule, function: walletNames },
- { ...arweaveConfigModule, function: arweaveConfig },
- { ...disconnectModule, function: disconnect, finalizer: disconnectFinalizer },
- { ...connectModule, function: connect },
- { ...signModule, function: sign, finalizer: signFinalizer },
- { ...dispatchModule, function: dispatch, finalizer: dispatchFinalizer },
- { ...encryptModule, function: encrypt, finalizer: encryptFinalizer },
- { ...decryptModule, function: decrypt, finalizer: decryptFinalizer },
- { ...signatureModule, function: signature, finalizer: signatureFinalizer },
- { ...addTokenModule, function: addToken },
- { ...isTokenAddedModule, function: isTokenAdded },
- {
- ...signMessageModule,
- function: signMessage,
- finalizer: signMessageFinalizer
- },
- {
- ...privateHashModule,
- function: privateHash,
- finalizer: privateHashFinalizer
- },
- { ...verifyMessageModule, function: verifyMessage },
- {
- ...signDataItemModule,
- function: signDataItem,
- finalizer: signDataItemFinalizer
- },
- { ...subscriptionModule, function: subscription },
- { ...userTokensModule, function: userTokens },
- { ...tokenBalanceModule, function: tokenBalance },
- {
- ...batchSignDataItemModule,
- function: batchSignDataItem,
- finalizer: batchSignDataItemFinalizer
- }
-];
-
-export default modules;
-
-/** Extended module interface */
-interface ForegroundModule extends Module {
- /**
- * A function that runs after results were
- * returned from the background script.
- * This is optional and will be ignored if not set.
- */
- finalizer?: ModuleFunction | TransformFinalizer;
-}
-
-/**
- * @param result The result from the background script
- * @param params The params the background script received
- * @param originalParams The params the injected function was called with
- */
-export type TransformFinalizer<
- ResultType,
- ParamsType = any,
- OriginalParamsType = any
-> = (
- result: ResultType,
- params: ParamsType,
- originalParams: OriginalParamsType
-) => any;
diff --git a/src/api/foreground/foreground-modules.ts b/src/api/foreground/foreground-modules.ts
new file mode 100644
index 000000000..64b5fef78
--- /dev/null
+++ b/src/api/foreground/foreground-modules.ts
@@ -0,0 +1,137 @@
+import type { Module, ModuleFunction } from "../module";
+
+// import modules
+import permissionsModule from "../modules/permissions";
+import permissions from "../modules/permissions/permissions.foreground";
+import activeAddressModule from "../modules/active_address";
+import activeAddress from "../modules/active_address/active_address.foreground";
+import allAddressesModule from "../modules/all_addresses";
+import allAddresses from "../modules/all_addresses/all_addresses.foreground";
+import publicKeyModule from "../modules/public_key";
+import publicKey from "../modules/public_key/public_key.foreground";
+import walletNamesModule from "../modules/wallet_names";
+import walletNames from "../modules/wallet_names/wallet_names.foreground";
+import arweaveConfigModule from "../modules/arweave_config";
+import arweaveConfig from "../modules/arweave_config/arweave_config.foreground";
+import disconnectModule from "../modules/disconnect";
+import disconnect, {
+ finalizer as disconnectFinalizer
+} from "../modules/disconnect/disconnect.foreground";
+import addTokenModule from "../modules/add_token";
+import addToken from "../modules/add_token/add_token.foreground";
+import isTokenAddedModule from "../modules/is_token_added";
+import isTokenAdded from "../modules/is_token_added/is_token_added.foreground";
+import connectModule from "../modules/connect";
+import connect from "../modules/connect/connect.foreground";
+import signModule from "../modules/sign";
+import sign, {
+ finalizer as signFinalizer
+} from "../modules/sign/sign.foreground";
+import dispatchModule from "../modules/dispatch";
+import dispatch, {
+ finalizer as dispatchFinalizer
+} from "../modules/dispatch/dispatch.foreground";
+import encryptModule from "../modules/encrypt";
+import encrypt, {
+ finalizer as encryptFinalizer
+} from "../modules/encrypt/encrypt.foreground";
+import decryptModule from "../modules/decrypt";
+import decrypt, {
+ finalizer as decryptFinalizer
+} from "../modules/decrypt/decrypt.foreground";
+import signatureModule from "../modules/signature";
+import signature, {
+ finalizer as signatureFinalizer
+} from "../modules/signature/signature.foreground";
+import signMessageModule from "../modules/sign_message";
+import signMessage, {
+ finalizer as signMessageFinalizer
+} from "../modules/sign_message/sign_message.foreground";
+import subscriptionModule from "../modules/subscription";
+import subscription from "../modules/subscription/subscription.foreground";
+import privateHashModule from "../modules/private_hash";
+import privateHash, {
+ finalizer as privateHashFinalizer
+} from "../modules/private_hash/private_hash.foreground";
+import verifyMessageModule from "../modules/verify_message";
+import verifyMessage from "../modules/verify_message/verify_message.foreground";
+import batchSignDataItemModule from "../modules/batch_sign_data_item";
+import batchSignDataItem, {
+ finalizer as batchSignDataItemFinalizer
+} from "../modules/batch_sign_data_item/batch_sign_data_item.foreground";
+import signDataItemModule from "../modules/sign_data_item";
+import signDataItem, {
+ finalizer as signDataItemFinalizer
+} from "../modules/sign_data_item/sign_data_item.foreground";
+import userTokensModule from "../modules/user_tokens";
+import userTokens from "../modules/user_tokens/user_tokens.foreground";
+import tokenBalanceModule from "../modules/token_balance";
+import tokenBalance from "../modules/token_balance/token_balance.foreground";
+
+/**
+ * @param result The result from the background script
+ * @param params The params the background script received
+ * @param originalParams The params the injected function was called with
+ */
+export type TransformFinalizer<
+ ResultType,
+ ParamsType = any,
+ OriginalParamsType = any
+> = (
+ result: ResultType,
+ params: ParamsType,
+ originalParams: OriginalParamsType
+) => any;
+
+/** Extended module interface */
+export interface ForegroundModule extends Module {
+ /**
+ * A function that runs after results were
+ * returned from the background script.
+ * This is optional and will be ignored if not set.
+ */
+ finalizer?: ModuleFunction | TransformFinalizer;
+}
+
+/** Foreground modules */
+export const foregroundModules: ForegroundModule[] = [
+ { ...permissionsModule, function: permissions },
+ { ...activeAddressModule, function: activeAddress },
+ { ...allAddressesModule, function: allAddresses },
+ { ...publicKeyModule, function: publicKey },
+ { ...walletNamesModule, function: walletNames },
+ { ...arweaveConfigModule, function: arweaveConfig },
+ { ...disconnectModule, function: disconnect, finalizer: disconnectFinalizer },
+ { ...connectModule, function: connect },
+ { ...signModule, function: sign, finalizer: signFinalizer },
+ { ...dispatchModule, function: dispatch, finalizer: dispatchFinalizer },
+ { ...encryptModule, function: encrypt, finalizer: encryptFinalizer },
+ { ...decryptModule, function: decrypt, finalizer: decryptFinalizer },
+ { ...signatureModule, function: signature, finalizer: signatureFinalizer },
+ { ...addTokenModule, function: addToken },
+ { ...isTokenAddedModule, function: isTokenAdded },
+ {
+ ...signMessageModule,
+ function: signMessage,
+ finalizer: signMessageFinalizer
+ },
+ {
+ ...privateHashModule,
+ function: privateHash,
+ finalizer: privateHashFinalizer
+ },
+ { ...verifyMessageModule, function: verifyMessage },
+ {
+ ...signDataItemModule,
+ function: signDataItem,
+ finalizer: signDataItemFinalizer
+ },
+ { ...subscriptionModule, function: subscription },
+ { ...userTokensModule, function: userTokens },
+ { ...tokenBalanceModule, function: tokenBalance },
+ {
+ ...batchSignDataItemModule,
+ function: batchSignDataItem,
+ finalizer: batchSignDataItemFinalizer
+ }
+];
diff --git a/src/contents/ar_protocol.ts b/src/api/foreground/foreground-setup-ar-protocol-links.ts
similarity index 82%
rename from src/contents/ar_protocol.ts
rename to src/api/foreground/foreground-setup-ar-protocol-links.ts
index c5b024807..78c919054 100644
--- a/src/contents/ar_protocol.ts
+++ b/src/api/foreground/foreground-setup-ar-protocol-links.ts
@@ -1,14 +1,9 @@
import { sendMessage } from "@arconnect/webext-bridge";
-import type { PlasmoCSConfig } from "plasmo";
import { isString } from "typed-assert";
-export const config: PlasmoCSConfig = {
- matches: ["file://*/*", "http://*/*", "https://*/*"],
- run_at: "document_start",
- all_frames: true
-};
+// TODO: This is not enough for client-rendered pages (e.g. React apps):
-document.addEventListener("DOMContentLoaded", async () => {
+export async function replaceArProtocolLinks() {
// all elements with the "ar://" protocol
const elements = document.querySelectorAll(
'a[href^="ar://"], img[src^="ar://"], iframe[src^="ar://"], ' +
@@ -16,6 +11,7 @@ document.addEventListener("DOMContentLoaded", async () => {
'link[href^="ar://"], embed[src^="ar://"], object[data^="ar://"],' +
'script[src^="ar://"]'
);
+
const fields = {
src: ["img", "iframe", "source", "embed", "script"],
href: ["a", "link"],
@@ -25,6 +21,7 @@ document.addEventListener("DOMContentLoaded", async () => {
for (const el of elements) {
// ask the background script to return the correct ar:// url
try {
+ // TODO: Replace with postMessage for the embedded wallet:
const res = await sendMessage(
"ar_protocol",
{ url: el[fields[el.tagName]] },
@@ -47,4 +44,4 @@ document.addEventListener("DOMContentLoaded", async () => {
console.error(`Failed to load ar:// resource: ${el[fields[el.tagName]]}`);
}
}
-});
+}
diff --git a/src/api/foreground/foreground-setup-events.ts b/src/api/foreground/foreground-setup-events.ts
new file mode 100644
index 000000000..c55a12639
--- /dev/null
+++ b/src/api/foreground/foreground-setup-events.ts
@@ -0,0 +1,52 @@
+import { onMessage } from "@arconnect/webext-bridge";
+
+// Some backend handlers (`src/api/background/handlers/*`) will use `sendMessage(...)` to communicate with the
+// `event.ts` content script, which in turn calls `postMessage()`, dispatches events or performs certain actions in the
+// content script's context.
+//
+// In ArConnect Embedded, instead of using `onMessage`, we should listen for messages coming from the iframe itself.
+// This also means that the background scripts, which in ArConnect Embedded run directly inside the iframe, need to be
+// updated to send messages using `postMessage`.
+//
+// See https://stackoverflow.com/questions/16266474/javascript-listen-for-postmessage-events-from-specific-iframe
+
+export function setupEventListeners(iframe?: HTMLIFrameElement) {
+ // event emitter events
+ onMessage("event", ({ data, sender }) => {
+ if (sender.context !== "background") return;
+
+ // send to mitt instance
+ postMessage({
+ type: "arconnect_event",
+ event: data
+ });
+ });
+
+ // listen for wallet switches
+ /** @deprecated */
+ onMessage("switch_wallet_event", ({ data, sender }) => {
+ if (sender.context !== "background") return;
+
+ // dispatch custom event
+ dispatchEvent(
+ new CustomEvent("walletSwitch", {
+ detail: { address: data }
+ })
+ );
+ });
+
+ // copy address in the content script
+ // (not possible in the background)
+ onMessage("copy_address", async ({ sender, data: addr }) => {
+ if (sender.context !== "background") return;
+
+ const input = document.createElement("input");
+
+ input.value = addr;
+
+ document.body.appendChild(input);
+ input.select();
+ document.execCommand("Copy");
+ document.body.removeChild(input);
+ });
+}
diff --git a/src/api/foreground/foreground-setup-wallet-sdk.ts b/src/api/foreground/foreground-setup-wallet-sdk.ts
new file mode 100644
index 000000000..c961f31ca
--- /dev/null
+++ b/src/api/foreground/foreground-setup-wallet-sdk.ts
@@ -0,0 +1,138 @@
+import type { ApiCall, ApiResponse, Event } from "shim";
+import type { InjectedEvents } from "~utils/events";
+import { nanoid } from "nanoid";
+import { foregroundModules } from "~api/foreground/foreground-modules";
+import mitt from "mitt";
+import { log, LOG_GROUP } from "~utils/log/log.utils";
+import { version } from "../../../package.json";
+
+export function setupWalletSDK(targetWindow: Window = window) {
+ log(LOG_GROUP.SETUP, "setupWalletSDK()");
+
+ /** Init events */
+ const events = mitt();
+
+ /** Init wallet API */
+ const WalletAPI: Record = {
+ walletName: "ArConnect",
+ walletVersion: version,
+ events
+ };
+
+ /** Inject each module */
+ for (const mod of foregroundModules) {
+ /** Handle foreground module and forward the result to the background */
+ WalletAPI[mod.functionName] = (...params: any[]) =>
+ new Promise(async (resolve, reject) => {
+ // execute foreground module
+ // TODO: Use a default function for those that do not have/need one and see if chunking can be done automatically or if it is needed at all:
+ const foregroundResult = await mod.function(...params);
+
+ // construct data to send to the background
+ const callID = nanoid();
+ const data: ApiCall & { ext: "arconnect" } = {
+ type: `api_${mod.functionName}`,
+ ext: "arconnect",
+ callID,
+ data: {
+ params: foregroundResult || params
+ }
+ };
+
+ // TODO: Replace `postMessage` with `isomorphicSendMessage`, which should be updated to handle
+ // chunking automatically based on data size, rather than relying on `sendChunk` to be called from
+ // the foreground scripts manually.
+
+ // Send message to background script (ArConnect Extension) or to the iframe window (ArConnect Embedded):
+ targetWindow.postMessage(data, window.location.origin);
+
+ // TODO: Note this is replacing the following from `api.content-script.ts`, so the logic to await and get the response is missing with just the
+ // one-line change above.
+ //
+ // const res = await sendMessage(
+ // data.type === "chunk" ? "chunk" : "api_call",
+ // data,
+ // "background"
+ // );
+ //
+ // window.postMessage(res, window.location.origin);
+
+ // wait for result from background
+ window.addEventListener("message", callback);
+
+ // TODO: Declare outside (factory) to facilitate testing?
+ async function callback(e: MessageEvent) {
+ // TODO: Make sure the response comes from targetWindow.
+ // See https://stackoverflow.com/questions/16266474/javascript-listen-for-postmessage-events-from-specific-iframe.
+
+ let { data: res } = e;
+
+ // validate return message
+ if (`${data.type}_result` !== res.type) return;
+
+ // only resolve when the result matching our callID is deleivered
+ if (data.callID !== res.callID) return;
+
+ window.removeEventListener("message", callback);
+
+ // check for errors
+ if (res.error) {
+ return reject(res.data);
+ }
+
+ // call the finalizer function if it exists
+ if (mod.finalizer) {
+ const finalizerResult = await mod.finalizer(
+ res.data,
+ foregroundResult,
+ params
+ );
+
+ // if the finalizer transforms data
+ // update the result
+ if (finalizerResult) {
+ res.data = finalizerResult;
+ }
+ }
+
+ // check for errors after the finalizer
+ if (res.error) {
+ return reject(res.data);
+ }
+
+ // resolve promise
+ return resolve(res.data);
+ }
+ });
+ }
+
+ // @ts-expect-error
+ window.arweaveWallet = WalletAPI;
+
+ // at the end of the injected script,
+ // we dispatch the wallet loaded event
+ dispatchEvent(new CustomEvent("arweaveWalletLoaded", { detail: {} }));
+
+ // send wallet loaded event again if page loaded
+ window.addEventListener("load", () => {
+ if (!window.arweaveWallet) return;
+ dispatchEvent(new CustomEvent("arweaveWalletLoaded", { detail: {} }));
+ });
+
+ // TODO: Remove it before to make sure there's no duplicate listener?
+
+ /** Handle events */
+ window.addEventListener(
+ "message",
+ (
+ e: MessageEvent<{
+ type: "arconnect_event";
+ event: Event;
+ }>
+ ) => {
+ if (e.data.type !== "arconnect_event") return;
+
+ events.emit(e.data.event.name, e.data.event.value);
+ }
+ );
+}
diff --git a/src/api/module.ts b/src/api/module.ts
index 12a12dddd..6458752a1 100644
--- a/src/api/module.ts
+++ b/src/api/module.ts
@@ -19,14 +19,14 @@ export interface ModuleProperties {
permissions: PermissionType[];
}
-/** Full API module (background/foreground) */
-export interface Module extends ModuleProperties {
- function: ModuleFunction;
-}
-
/**
* Function type for background and injected script API functions
*/
export type ModuleFunction = (
...params: any[]
) => Promise | ResultType;
+
+/** Full API module (background/foreground) */
+export interface Module extends ModuleProperties {
+ function: ModuleFunction;
+}
diff --git a/src/api/modules/README.md b/src/api/modules/README.md
index edb38fd97..685110070 100644
--- a/src/api/modules/README.md
+++ b/src/api/modules/README.md
@@ -13,3 +13,17 @@ Each module has to be added separately in the two module files (`background.ts`
### Examples
For basic examples on how to create a module, refer to the [example module](example/).
+
+## Message Passing
+
+### ArConnect Browser Extension
+
+There are 3 contexts here:
+
+- Background scripts (service worker).
+- Content scripts (injected into the page but with its own isolated context).
+- Injected scripts (injected into the page in a `