{#if $authSignedIn}
{/if}
diff --git a/src/frontend/src/lib/components/hero/Hero.svelte b/src/frontend/src/lib/components/hero/Hero.svelte
index 813bff661..07538f80f 100644
--- a/src/frontend/src/lib/components/hero/Hero.svelte
+++ b/src/frontend/src/lib/components/hero/Hero.svelte
@@ -14,11 +14,11 @@
$: heroContent = usdTotal || summary;
-
+
{#if $authSignedIn}
diff --git a/src/frontend/src/lib/components/hero/HeroContent.svelte b/src/frontend/src/lib/components/hero/HeroContent.svelte
index 08aa67337..0e006a3b7 100644
--- a/src/frontend/src/lib/components/hero/HeroContent.svelte
+++ b/src/frontend/src/lib/components/hero/HeroContent.svelte
@@ -1,4 +1,5 @@
-
-
diff --git a/src/frontend/src/lib/components/icons/OisyWalletLogo.svelte b/src/frontend/src/lib/components/icons/OisyWalletLogo.svelte
index bb5aba42f..f1da67def 100644
--- a/src/frontend/src/lib/components/icons/OisyWalletLogo.svelte
+++ b/src/frontend/src/lib/components/icons/OisyWalletLogo.svelte
@@ -1,6 +1,7 @@
-
+ {#if action}
+
+
+
+ {/if}
diff --git a/src/frontend/src/lib/components/ui/ContentWithToolbar.svelte b/src/frontend/src/lib/components/ui/ContentWithToolbar.svelte
index 628c188a2..a52855000 100644
--- a/src/frontend/src/lib/components/ui/ContentWithToolbar.svelte
+++ b/src/frontend/src/lib/components/ui/ContentWithToolbar.svelte
@@ -1,3 +1,5 @@
-
+
+
+
diff --git a/src/frontend/src/lib/components/ui/HeroButtonGroup.svelte b/src/frontend/src/lib/components/ui/HeroButtonGroup.svelte
index a6bb88ba8..431d3cd1f 100644
--- a/src/frontend/src/lib/components/ui/HeroButtonGroup.svelte
+++ b/src/frontend/src/lib/components/ui/HeroButtonGroup.svelte
@@ -1,6 +1,6 @@
diff --git a/src/frontend/src/lib/components/ui/WarningBanner.svelte b/src/frontend/src/lib/components/ui/WarningBanner.svelte
new file mode 100644
index 000000000..cd92637ba
--- /dev/null
+++ b/src/frontend/src/lib/components/ui/WarningBanner.svelte
@@ -0,0 +1,10 @@
+
+
+
+
+
+
diff --git a/src/frontend/src/lib/constants/app.constants.ts b/src/frontend/src/lib/constants/app.constants.ts
index 4c3f326f6..8bfb3dd2d 100644
--- a/src/frontend/src/lib/constants/app.constants.ts
+++ b/src/frontend/src/lib/constants/app.constants.ts
@@ -11,6 +11,8 @@ export const PROD = MODE === 'ic';
const MAINNET_DOMAIN = 'icp0.io';
+export const REPLICA_HOST = LOCAL ? 'http://localhost:4943/' : 'https://icp-api.io';
+
export const INTERNET_IDENTITY_CANISTER_ID = LOCAL
? import.meta.env.VITE_LOCAL_INTERNET_IDENTITY_CANISTER_ID
: undefined;
@@ -85,3 +87,7 @@ export const NANO_SECONDS_IN_MINUTE = NANO_SECONDS_IN_MILLISECOND * 1_000n * 60n
export const EIGHT_DECIMALS = 8;
export const ZERO = BigNumber.from(0n);
+
+// Wallets
+export const WALLET_TIMER_INTERVAL_MILLIS = (SECONDS_IN_MINUTE / 2) * 1000; // 30 seconds in milliseconds
+export const WALLET_PAGINATION = 10n;
diff --git a/src/frontend/src/lib/derived/address.derived.ts b/src/frontend/src/lib/derived/address.derived.ts
index 53d4a6048..269d56e20 100644
--- a/src/frontend/src/lib/derived/address.derived.ts
+++ b/src/frontend/src/lib/derived/address.derived.ts
@@ -1,5 +1,6 @@
import {
btcAddressMainnetStore,
+ btcAddressRegtestStore,
btcAddressTestnetStore,
ethAddressStore
} from '$lib/stores/address.store';
@@ -28,6 +29,11 @@ export const btcAddressTestnet: Readable
= derived(
([$btcAddressTestnetStore]) => mapAddress($btcAddressTestnetStore)
);
+export const btcAddressRegtest: Readable = derived(
+ [btcAddressRegtestStore],
+ ([$btcAddressRegtestStore]) => mapAddress($btcAddressRegtestStore)
+);
+
export const ethAddress: Readable = derived(
[ethAddressStore],
([$ethAddressStore]) => mapAddress($ethAddressStore)
diff --git a/src/frontend/src/lib/derived/exchange.derived.ts b/src/frontend/src/lib/derived/exchange.derived.ts
index 1649c4d71..b0d5b7e54 100644
--- a/src/frontend/src/lib/derived/exchange.derived.ts
+++ b/src/frontend/src/lib/derived/exchange.derived.ts
@@ -1,4 +1,8 @@
-import { BTC_MAINNET_TOKEN_ID } from '$env/tokens.btc.env';
+import {
+ BTC_MAINNET_TOKEN_ID,
+ BTC_REGTEST_TOKEN_ID,
+ BTC_TESTNET_TOKEN_ID
+} from '$env/tokens.btc.env';
import { ETHEREUM_TOKEN_ID, ICP_TOKEN_ID, SEPOLIA_TOKEN_ID } from '$env/tokens.env';
import { enabledErc20Tokens } from '$eth/derived/erc20.derived';
import type { Erc20Token } from '$eth/types/erc20';
@@ -21,7 +25,10 @@ export const exchanges: Readable = derived(
const icpPrice = $exchangeStore?.['internet-computer'];
return {
+ // TODO: improve feed price on BTC testnet, for now we assume that 1 BTC mainnet = 1 BTC testnet
+ [BTC_TESTNET_TOKEN_ID]: btcPrice,
[BTC_MAINNET_TOKEN_ID]: btcPrice,
+ [BTC_REGTEST_TOKEN_ID]: btcPrice,
[ETHEREUM_TOKEN_ID]: ethPrice,
[SEPOLIA_TOKEN_ID]: ethPrice,
[ICP_TOKEN_ID]: icpPrice,
diff --git a/src/frontend/src/lib/derived/modal.derived.ts b/src/frontend/src/lib/derived/modal.derived.ts
index dd30d86c7..04496a562 100644
--- a/src/frontend/src/lib/derived/modal.derived.ts
+++ b/src/frontend/src/lib/derived/modal.derived.ts
@@ -33,10 +33,6 @@ export const modalBuy: Readable = derived(
modalStore,
($modalStore) => $modalStore?.type === 'buy'
);
-export const modalEthSend: Readable = derived(
- modalStore,
- ($modalStore) => $modalStore?.type === 'eth-send'
-);
export const modalConvertCkBTCToBTC: Readable = derived(
modalStore,
($modalStore) => $modalStore?.type === 'convert-ckbtc-btc'
@@ -53,10 +49,6 @@ export const modalHowToConvertToTwinTokenEth: Readable = derived(
modalStore,
($modalStore) => $modalStore?.type === 'how-to-convert-to-twin-token-eth'
);
-export const modalIcSend: Readable = derived(
- modalStore,
- ($modalStore) => $modalStore?.type === 'ic-send'
-);
export const modalWalletConnectAuth: Readable = derived(
modalStore,
($modalStore) => $modalStore?.type === 'wallet-connect-auth'
diff --git a/src/frontend/src/lib/derived/tokens.derived.ts b/src/frontend/src/lib/derived/tokens.derived.ts
index 9167adb09..72565d5ce 100644
--- a/src/frontend/src/lib/derived/tokens.derived.ts
+++ b/src/frontend/src/lib/derived/tokens.derived.ts
@@ -51,8 +51,17 @@ export const enabledErc20Tokens: Readable = derived(
$enabledTokens.filter(({ standard }) => standard === 'erc20') as Erc20Token[]
);
+// TODO: add tests when https://github.com/dfinity/oisy-wallet/pull/2450 is merged
/**
- * The following store is use as reference for the list of WalletWorkers that are started/stopped in the main token page.
+ * The following store is used as reference for the list of WalletWorkers that are started/stopped in the main token page.
+ */
+export const enabledBtcTokens: Readable = derived(
+ [enabledBitcoinTokens],
+ filterEnabledTokens
+);
+
+/**
+ * The following store is used as reference for the list of WalletWorkers that are started/stopped in the main token page.
*/
// TODO: The several dependencies of enabledIcTokens are not strictly only IC tokens, but other tokens too.
// We should find a better way to handle this, improving the store.
diff --git a/src/frontend/src/lib/i18n/en.json b/src/frontend/src/lib/i18n/en.json
index 8c38d0915..ee8f0792d 100644
--- a/src/frontend/src/lib/i18n/en.json
+++ b/src/frontend/src/lib/i18n/en.json
@@ -13,14 +13,15 @@
"decimals": "Decimals",
"amount": "Amount",
"max": "Max",
- "more": "More"
+ "more": "More",
+ "reject": "Reject",
+ "approve": "Approve"
},
"info": {
"test_banner": "For testing purposes only!"
},
"alt": {
"logo": "$name logo",
- "background": "Background image of $oisy_name with gradient logo",
"go_to_home": "Go to the $oisy_name home page"
}
},
@@ -29,21 +30,21 @@
"source_code_on_github": "Source code on GitHub",
"view_on_explorer": "View on explorer",
"source_code": "Source code",
- "oisy_roadmap": "$oisy_name Roadmap",
+ "changelog": "Changelog",
"submit_ticket": "Submit a ticket"
},
"alt": {
"more_settings": "More settings",
"menu": "Your wallet address, settings, sign-out and external links",
- "oisy_roadmap": "Open the $oisy_name roadmap",
- "submit_ticket": "Open the ticket submission form"
+ "changelog": "Open the changelog of $oisy_name on GitHub to review the latest updates",
+ "submit_ticket": "Open the ticket submission form on GitHub to report an issue or request a feature"
}
},
"auth": {
"text": {
- "title_part_1": "A wallet that lives on-chain for",
- "title_part_2": "100% decentralization",
- "authenticate": "Authenticate",
+ "title_part_1": "Multi-chain wallet.",
+ "title_part_2": "100% onchain.",
+ "authenticate": "Open | Create",
"logout": "Logout"
},
"alt": {
@@ -459,8 +460,6 @@
"text": {
"name": "WalletConnect",
"session_proposal": "Session Proposal",
- "approve": "Approve",
- "reject": "Reject",
"connect": "Connect",
"connecting": "Connecting...",
"disconnect": "Disconnect",
diff --git a/src/frontend/src/lib/rest/goincecko.rest.ts b/src/frontend/src/lib/rest/coingecko.rest.ts
similarity index 100%
rename from src/frontend/src/lib/rest/goincecko.rest.ts
rename to src/frontend/src/lib/rest/coingecko.rest.ts
diff --git a/src/frontend/src/icp/schedulers/scheduler.ts b/src/frontend/src/lib/schedulers/scheduler.ts
similarity index 100%
rename from src/frontend/src/icp/schedulers/scheduler.ts
rename to src/frontend/src/lib/schedulers/scheduler.ts
diff --git a/src/frontend/src/lib/services/address.services.ts b/src/frontend/src/lib/services/address.services.ts
index 0c4611d0c..9f4f3a891 100644
--- a/src/frontend/src/lib/services/address.services.ts
+++ b/src/frontend/src/lib/services/address.services.ts
@@ -1,6 +1,9 @@
-import type { BitcoinNetwork as SignerBitcoinNetwork } from '$declarations/signer/signer.did';
import { NETWORK_BITCOIN_ENABLED } from '$env/networks.btc.env';
-import { BTC_MAINNET_TOKEN_ID, BTC_TESTNET_TOKEN_ID } from '$env/tokens.btc.env';
+import {
+ BTC_MAINNET_TOKEN_ID,
+ BTC_REGTEST_TOKEN_ID,
+ BTC_TESTNET_TOKEN_ID
+} from '$env/tokens.btc.env';
import { ETHEREUM_TOKEN_ID } from '$env/tokens.env';
import {
getIdbBtcAddressMainnet,
@@ -15,6 +18,7 @@ import { getBtcAddress, getEthAddress } from '$lib/api/signer.api';
import { warnSignOut } from '$lib/services/auth.services';
import {
btcAddressMainnetStore,
+ btcAddressRegtestStore,
btcAddressTestnetStore,
ethAddressStore,
type AddressStore,
@@ -30,18 +34,19 @@ import type { OptionIdentity } from '$lib/types/identity';
import type { TokenId } from '$lib/types/token';
import type { ResultSuccess, ResultSuccessReduced } from '$lib/types/utils';
import { replacePlaceholders } from '$lib/utils/i18n.utils';
+import { mapToSignerBitcoinNetwork } from '$lib/utils/network.utils';
import { reduceResults } from '$lib/utils/results.utils';
import type { BitcoinNetwork } from '@dfinity/ckbtc';
import type { Principal } from '@dfinity/principal';
-import { assertNonNullish, isNullish } from '@dfinity/utils';
+import { assertNonNullish, isNullish, nonNullish } from '@dfinity/utils';
import { get } from 'svelte/store';
-type LoadTokenAddressParams = {
+interface LoadTokenAddressParams {
tokenId: TokenId;
getAddress: (identity: OptionIdentity) => Promise;
- setIdbAddress: (params: SetIdbAddressParams) => Promise;
+ setIdbAddress: ((params: SetIdbAddressParams) => Promise) | null;
addressStore: AddressStore;
-};
+}
const loadTokenAddress = async ({
tokenId,
@@ -55,7 +60,9 @@ const loadTokenAddress = async ({
const address = await getAddress(identity);
addressStore.set({ data: address, certified: true });
- await saveTokenAddressForFutureSignIn({ address, identity, setIdbAddress });
+ if (nonNullish(setIdbAddress)) {
+ await saveTokenAddressForFutureSignIn({ address, identity, setIdbAddress });
+ }
} catch (err: unknown) {
addressStore.reset();
@@ -75,11 +82,13 @@ const loadTokenAddress = async ({
};
// We use the Testnet address for Regtest.
-type TokenIdBtcPublicNetwork = typeof BTC_MAINNET_TOKEN_ID | typeof BTC_TESTNET_TOKEN_ID;
-type BtcPublicNetwork = Exclude;
+type TokenIdBtcPublicNetwork =
+ | typeof BTC_MAINNET_TOKEN_ID
+ | typeof BTC_TESTNET_TOKEN_ID
+ | typeof BTC_REGTEST_TOKEN_ID;
const bitcoinMapper: Record<
- BtcPublicNetwork,
+ BitcoinNetwork,
Pick, 'addressStore' | 'setIdbAddress'>
> = {
mainnet: {
@@ -89,27 +98,27 @@ const bitcoinMapper: Record<
testnet: {
addressStore: btcAddressTestnetStore,
setIdbAddress: setIdbBtcAddressTestnet
+ },
+ regtest: {
+ addressStore: btcAddressRegtestStore,
+ // No need to store the regtest in the local storage because it's only used locally.
+ setIdbAddress: null
}
};
-const bitcoinNetworkMapper: Record = {
- mainnet: { mainnet: null },
- testnet: { testnet: null }
-};
-
const loadBtcAddress = async ({
tokenId,
network
}: {
tokenId: TokenIdBtcPublicNetwork;
- network: BtcPublicNetwork;
+ network: BitcoinNetwork;
}): Promise =>
loadTokenAddress({
tokenId,
getAddress: (identity: OptionIdentity) =>
getBtcAddress({
identity,
- network: bitcoinNetworkMapper[network],
+ network: mapToSignerBitcoinNetwork({ network }),
nullishIdentityErrorMessage: get(i18n).auth.error.no_internet_identity
}),
...bitcoinMapper[network]
@@ -121,6 +130,12 @@ export const loadBtcAddressTestnet = async (): Promise =>
network: 'testnet'
});
+export const loadBtcAddressRegtest = async (): Promise =>
+ loadBtcAddress({
+ tokenId: BTC_REGTEST_TOKEN_ID,
+ network: 'regtest'
+ });
+
const loadBtcAddressMainnet = async (): Promise =>
loadBtcAddress({
tokenId: BTC_MAINNET_TOKEN_ID,
diff --git a/src/frontend/src/lib/services/exchange.services.ts b/src/frontend/src/lib/services/exchange.services.ts
index 1b6b6865a..04067d7d3 100644
--- a/src/frontend/src/lib/services/exchange.services.ts
+++ b/src/frontend/src/lib/services/exchange.services.ts
@@ -1,6 +1,6 @@
import type { Erc20ContractAddress } from '$eth/types/erc20';
import type { LedgerCanisterIdText } from '$icp/types/canister';
-import { simplePrice, simpleTokenPrice } from '$lib/rest/goincecko.rest';
+import { simplePrice, simpleTokenPrice } from '$lib/rest/coingecko.rest';
import { exchangeStore } from '$lib/stores/exchange.store';
import type {
CoingeckoSimplePriceResponse,
diff --git a/src/frontend/src/lib/services/load-user-profile.services.ts b/src/frontend/src/lib/services/load-user-profile.services.ts
index 873675be6..7dbb13c50 100644
--- a/src/frontend/src/lib/services/load-user-profile.services.ts
+++ b/src/frontend/src/lib/services/load-user-profile.services.ts
@@ -15,7 +15,11 @@ const queryProfile = async ({
identity: OptionIdentity;
certified: boolean;
}): Promise => {
- const response = await getUserProfile({ identity, certified });
+ const response = await getUserProfile({
+ identity,
+ certified,
+ nullishIdentityErrorMessage: get(i18n).auth.error.no_internet_identity
+ });
if ('Ok' in response) {
return response.Ok;
}
@@ -63,7 +67,10 @@ export const loadUserProfile = async ({
try {
let profile = await queryUnsafeProfile({ identity });
if (isNullish(profile)) {
- profile = await createUserProfile({ identity });
+ profile = await createUserProfile({
+ identity,
+ nullishIdentityErrorMessage: get(i18n).auth.error.no_internet_identity
+ });
userProfileStore.set({ certified: true, profile });
} else {
// We set the store before the call to load the certified profile.
diff --git a/src/frontend/src/lib/services/request-pouh-credential.services.ts b/src/frontend/src/lib/services/request-pouh-credential.services.ts
index 131790297..f4e1d8827 100644
--- a/src/frontend/src/lib/services/request-pouh-credential.services.ts
+++ b/src/frontend/src/lib/services/request-pouh-credential.services.ts
@@ -7,6 +7,7 @@ import {
VC_POPUP_WIDTH
} from '$lib/constants/app.constants';
import { POUH_CREDENTIAL_TYPE } from '$lib/constants/credentials.constants';
+import { loadCertifiedUserProfile } from '$lib/services/load-user-profile.services';
import { i18n } from '$lib/stores/i18n.store';
import { toastsError } from '$lib/stores/toasts.store';
import { userProfileStore } from '$lib/stores/user-profile.store';
@@ -22,7 +23,6 @@ import {
type VerifiablePresentationResponse
} from '@dfinity/verifiable-credentials/request-verifiable-presentation';
import { get } from 'svelte/store';
-import { loadCertifiedUserProfile } from './load-user-profile.services';
const addPouhCredential = async ({
identity,
@@ -44,7 +44,8 @@ const addPouhCredential = async ({
arguments: []
},
issuerCanisterId,
- currentUserVersion: fromNullable(userProfile?.profile.version ?? [])
+ currentUserVersion: fromNullable(userProfile?.profile.version ?? []),
+ nullishIdentityErrorMessage: get(i18n).auth.error.no_internet_identity
});
if ('Ok' in response) {
return { success: true };
diff --git a/src/frontend/src/lib/stores/address.store.ts b/src/frontend/src/lib/stores/address.store.ts
index 18460a63e..3a5308f93 100644
--- a/src/frontend/src/lib/stores/address.store.ts
+++ b/src/frontend/src/lib/stores/address.store.ts
@@ -22,7 +22,7 @@ const initAddressStore = (): AddressStore => {
};
};
-// We use the Testnet address for Regtest.
+export const btcAddressRegtestStore = initAddressStore();
export const btcAddressTestnetStore = initAddressStore();
export const btcAddressMainnetStore = initAddressStore();
diff --git a/src/frontend/src/lib/stores/modal.store.ts b/src/frontend/src/lib/stores/modal.store.ts
index 63a6b0904..8d94a46bb 100644
--- a/src/frontend/src/lib/stores/modal.store.ts
+++ b/src/frontend/src/lib/stores/modal.store.ts
@@ -11,12 +11,10 @@ export interface Modal {
| 'receive'
| 'send'
| 'buy'
- | 'eth-send'
| 'convert-ckbtc-btc'
| 'convert-to-twin-token-cketh'
| 'convert-to-twin-token-eth'
| 'how-to-convert-to-twin-token-eth'
- | 'ic-send'
| 'wallet-connect-auth'
| 'wallet-connect-sign'
| 'wallet-connect-send'
@@ -44,12 +42,10 @@ export interface ModalStore extends Readable> {
openReceive: (data: D) => void;
openSend: (data: D) => void;
openBuy: (data: D) => void;
- openEthSend: (data: D) => void;
openConvertCkBTCToBTC: () => void;
openConvertToTwinTokenCkEth: () => void;
openConvertToTwinTokenEth: () => void;
openHowToConvertToTwinTokenEth: () => void;
- openIcSend: (data: D) => void;
openWalletConnectAuth: () => void;
openWalletConnectSign: (data: D) => void;
openWalletConnectSend: (data: D) => void;
@@ -78,12 +74,10 @@ const initModalStore = (): ModalStore => {
openReceive: (data: D) => set({ type: 'receive', data }),
openSend: (data: D) => set({ type: 'send', data }),
openBuy: (data: D) => set({ type: 'buy', data }),
- openEthSend: (data: D) => set({ type: 'eth-send', data }),
openConvertCkBTCToBTC: () => set({ type: 'convert-ckbtc-btc' }),
openConvertToTwinTokenCkEth: () => set({ type: 'convert-to-twin-token-cketh' }),
openConvertToTwinTokenEth: () => set({ type: 'convert-to-twin-token-eth' }),
openHowToConvertToTwinTokenEth: () => set({ type: 'how-to-convert-to-twin-token-eth' }),
- openIcSend: (data: D) => set({ type: 'ic-send', data }),
openWalletConnectAuth: () => set({ type: 'wallet-connect-auth' }),
openWalletConnectSign: (data: D) => set({ type: 'wallet-connect-sign', data }),
openWalletConnectSend: (data: D) => set({ type: 'wallet-connect-send', data }),
diff --git a/src/frontend/src/lib/stores/user-profile.store.ts b/src/frontend/src/lib/stores/user-profile.store.ts
index 9effdccf5..7f1b34051 100644
--- a/src/frontend/src/lib/stores/user-profile.store.ts
+++ b/src/frontend/src/lib/stores/user-profile.store.ts
@@ -2,10 +2,10 @@ import type { UserProfile } from '$declarations/backend/backend.did';
import type { Option } from '$lib/types/utils';
import { writable, type Readable } from 'svelte/store';
-type CertifiedUserProfileData = {
+interface CertifiedUserProfileData {
profile: UserProfile;
certified: boolean;
-};
+}
// * `undefined` means the store is not loaded yet.
// * `null` means there was an error.
diff --git a/src/frontend/src/lib/styles/global/body.scss b/src/frontend/src/lib/styles/global/body.scss
index 73509c784..5a67988de 100644
--- a/src/frontend/src/lib/styles/global/body.scss
+++ b/src/frontend/src/lib/styles/global/body.scss
@@ -1,6 +1,6 @@
:root {
background: var(--color-transparent);
- color: var(--color-dark);
+ color: var(--color-secondary);
}
body {
diff --git a/src/frontend/src/lib/styles/global/button.scss b/src/frontend/src/lib/styles/global/button.scss
index 8f0c65dab..4cd3dcb0d 100644
--- a/src/frontend/src/lib/styles/global/button.scss
+++ b/src/frontend/src/lib/styles/global/button.scss
@@ -11,7 +11,7 @@ button {
cursor: pointer;
}
- --button-border-radius: var(--border-radius-xs);
+ --button-border-radius: var(--border-radius-md);
border-radius: var(--button-border-radius);
transition:
@@ -21,24 +21,25 @@ button {
border: 1px solid transparent;
- --button-padding: var(--padding) var(--padding-3x);
+ --button-padding: var(--padding-2x) var(--padding-3x);
&.primary,
&.secondary,
- &.tertiary {
+ &.secondary-alt {
justify-content: flex-start;
padding: var(--button-padding);
}
- &.secondary,
+ // TODO: check with Design team if this class is still needed
+ &.secondary-alt,
&.user {
border: 1px solid var(--color-blue);
}
&.primary,
&.secondary,
- &.tertiary {
+ &.secondary-alt {
font-weight: var(--font-weight-bold);
&[disabled],
@@ -46,26 +47,53 @@ button {
background: var(--color-grey);
color: var(--color-white);
opacity: 1;
+ cursor: not-allowed;
+
+ &:hover,
+ &:active {
+ background: var(--color-grey);
+ color: var(--color-white);
+ }
+ }
+
+ &[focus],
+ &:focus {
+ outline: none;
+ box-shadow:
+ 0 0 0 2px var(--color-white),
+ 0 0 0 4px var(--color-blue-ribbon);
}
}
&.primary {
- background: var(--color-brandeis-blue);
+ background: var(--color-blue-ribbon);
color: var(--color-white);
&:hover {
- background: var(--color-brilliant-azure);
+ background: var(--color-cobalt);
+ }
+
+ &:active {
+ background: var(--color-resolution-blue);
}
}
&.secondary {
- background: var(--color-cetacean-blue);
- color: var(--color-white);
+ background: var(--color-zumthor);
+ color: var(--color-primary);
+
+ &:hover {
+ background: var(--color-onahau);
+ }
+
+ &:active {
+ background: var(--color-anakiwa);
+ }
}
- &.tertiary {
- background: var(--color-alice-blue);
- color: var(--color-primary);
+ &.secondary-alt {
+ background: var(--color-cetacean-blue);
+ color: var(--color-white);
}
&.hero {
@@ -81,7 +109,7 @@ button {
&.primary,
&.secondary,
- &.tertiary {
+ &.secondary-alt {
&.full {
width: 100%;
}
diff --git a/src/frontend/src/lib/styles/global/colors.scss b/src/frontend/src/lib/styles/global/colors.scss
index e5c270c28..6de37e22e 100644
--- a/src/frontend/src/lib/styles/global/colors.scss
+++ b/src/frontend/src/lib/styles/global/colors.scss
@@ -2,10 +2,11 @@
// Figma color names
--color-off-white: theme(colors.off-white);
--color-white: theme(colors.white);
+ --color-white-rgb: theme(colors.white-rgb);
+ --color-black-rgb: theme(colors.black-rgb);
--color-grey: theme(colors.grey);
--color-dust: theme(colors.dust);
--color-blue: theme(colors.blue);
- --color-dark: theme(colors.dark);
--color-dark-blue: theme(colors.dark-blue);
--color-misty-rose: theme(colors.misty-rose);
@@ -15,12 +16,19 @@
--color-mountain-meadow: theme(colors.mountain-meadow);
--color-brandeis-blue: theme(colors.brandeis-blue);
--color-cetacean-blue: theme(colors.cetacean-blue);
+ --color-pale-cornflower-blue: theme(colors.pale-cornflower-blue);
--color-brilliant-azure: theme(colors.brilliant-azure);
--color-blue-ribbon-rgb: theme(colors.blue-ribbon-rgb);
+ --color-blue-ribbon: rgb(var(--color-blue-ribbon-rgb));
--color-alice-blue: theme(colors.alice-blue);
--color-american-orange: theme(colors.american-orange);
--color-crayola-yellow: theme(colors.crayola-yellow);
--color-cornsilk: theme(colors.cornsilk);
+ --color-cobalt: theme(colors.cobalt);
+ --color-resolution-blue: theme(colors.resolution-blue);
+ --color-zumthor: theme(colors.zumthor);
+ --color-onahau: theme(colors.onahau);
+ --color-anakiwa: theme(colors.anakiwa);
// Brand
--color-wallet-connect: #3b99fc;
@@ -28,4 +36,6 @@
// Theme
--color-primary-rgb: var(--color-blue-ribbon-rgb);
--color-primary: rgb(var(--color-primary-rgb));
+ --color-secondary-rgb: var(--color-black-rgb);
+ --color-secondary: rgb(var(--color-secondary-rgb));
}
diff --git a/src/frontend/src/lib/styles/global/gix.scss b/src/frontend/src/lib/styles/global/gix.scss
index 610bb9c2f..36da94e3e 100644
--- a/src/frontend/src/lib/styles/global/gix.scss
+++ b/src/frontend/src/lib/styles/global/gix.scss
@@ -10,9 +10,9 @@
--primary: var(--color-blue);
--primary-contrast: var(--color-white);
- --tertiary: var(--color-dark);
+ --tertiary: var(--color-secondary);
- --value-color: var(--color-dark);
+ --value-color: var(--color-secondary);
--border-radius-2x: var(--border-radius-lg);
--border-radius-0_5x: var(--border-radius-xs);
@@ -25,7 +25,7 @@
--progress-color-rgb: var(--color-primary-rgb);
--overlay-background: var(--color-white);
- --overlay-background-contrast: var(--color-dark);
+ --overlay-background-contrast: var(--color-secondary);
--dropdown-background: var(--overlay-background);
--dropdown-border-size: 1.5px;
@@ -50,7 +50,7 @@
--font-size-standard: 1rem;
// Segment
- --segment-selected-background: var(--color-dark);
+ --segment-selected-background: var(--color-secondary);
// Modal
@@ -58,20 +58,22 @@
--alert-max-width: calc(100vw - var(--padding-4x));
--alert-max-height: calc(100vh - var(--padding-4x));
--alert-border-radius: var(--border-radius-xs);
- --alert-padding-y: var(--padding-4x);
- --alert-padding-x: var(--padding-2x);
-
- --dialog-width: 100vw;
- --dialog-max-width: 100vw;
- --dialog-height: 100vh;
- --dialog-border-radius: 0;
- --dialog-padding-y: var(--padding-3x);
- --dialog-padding-x: var(--padding-3x);
+ --alert-padding-y: 0;
+ --alert-padding-x: 0;
+
+ --dialog-width: 464px;
+ --dialog-max-width: var(--alert-max-width);
+ --dialog-max-height: var(--alert-max-height);
+ --dialog-min-height: calc(100vh - var(--padding-4x));
+ --dialog-height: initial;
+ --dialog-border-radius: calc(var(--border-radius-sm) * 3);
+ --dialog-padding-y: 0;
+ --dialog-padding-x: 0;
// CSS variables used when the content needs to fit the maximum height as when a QR-Code reader is embedded in a modal
--dialog-header-height: 74px;
- --backdrop-contrast: var(--color-dark);
+ --backdrop-contrast: var(--color-secondary);
--backdrop-filter: blur(4px);
@supports (-webkit-touch-callout: none) {
@@ -82,13 +84,6 @@
@include media.min-width(medium) {
--alert-width: 464px;
- --dialog-width: 464px;
- --dialog-max-width: var(--alert-max-width);
- --dialog-min-height: calc(100vh - var(--padding-8x));
- --dialog-height: initial;
- --dialog-max-height: var(--alert-max-height);
- --dialog-border-radius: var(--alert-border-radius);
-
--section-max-width: 576px;
}
@@ -117,23 +112,41 @@ div.input-field input[id]::placeholder {
}
div.modal {
- --overlay-background: var(--color-cetacean-blue);
- --overlay-background-contrast: var(--color-dark);
+ --overlay-background: var(--color-white);
+ --overlay-background-contrast: var(--color-secondary);
--overlay-content-background: var(--color-white);
- --overlay-content-background-contrast: var(--color-dark);
+ --overlay-content-background-contrast: var(--color-secondary);
- color: var(--color-dark);
+ color: var(--color-secondary);
font-weight: var(--font-weight-normal);
div.wrapper.dialog {
- color: var(--color-white);
+ color: var(--color-secondary);
+ padding-block: var(--padding-3x);
+ padding-inline: var(--padding-0_5x);
+ }
+
+ div.wrapper.dialog div.header {
+ // TODO: Improve padding definition when the Modal of GIX Components has an updated way of setting it and not being hard-coded (https://github.com/dfinity/gix-components/blob/1c4ab390f9cab1d0e3ec73a23384e045679eb6b8/src/lib/components/Modal.svelte#L195)
+ --padding-3x: 0;
+ --padding: var(--padding-2x);
+ --dialog-padding-y: 0;
+
+ margin-inline: calc(3 / 2 * var(--padding));
+
+ --color-grey: rgba(0, 0, 0, 0.05);
+ border-bottom: 1px solid var(--color-grey);
}
div.wrapper.dialog div.container-wrapper {
- margin: var(--padding-1_5x) var(--padding-1_5x) auto;
+ margin: var(--padding-3x) 0 0;
div.container {
- border-radius: var(--border-radius-0_5x);
+ border-radius: 0;
+
+ div.content {
+ padding-inline: var(--padding-2_5x);
+ }
}
}
@@ -151,6 +164,7 @@ div.modal {
div.transition:not(:has(+ form)),
div.transition > form {
display: flex;
+ flex: 1;
flex-direction: column;
min-height: 100%;
@@ -194,3 +208,13 @@ div.segment-button {
color: var(--color-white);
}
}
+
+div.step.completed {
+ svg {
+ --icon-check-circle-background: var(--color-primary);
+ }
+
+ div.line {
+ --line-color: var(--color-primary);
+ }
+}
diff --git a/src/frontend/src/lib/styles/global/layout.scss b/src/frontend/src/lib/styles/global/layout.scss
index 0549ecd7c..f876bffd2 100644
--- a/src/frontend/src/lib/styles/global/layout.scss
+++ b/src/frontend/src/lib/styles/global/layout.scss
@@ -3,7 +3,7 @@
main,
.main {
margin: 0 auto;
- padding-inline: calc(2.5 * var(--padding));
+ padding-inline: var(--padding-2_5x);
max-width: media.$breakpoint-small;
}
diff --git a/src/frontend/src/lib/styles/global/text.scss b/src/frontend/src/lib/styles/global/text.scss
index 01120568e..6fa974f75 100644
--- a/src/frontend/src/lib/styles/global/text.scss
+++ b/src/frontend/src/lib/styles/global/text.scss
@@ -11,3 +11,7 @@
.text-primary {
color: var(--color-primary);
}
+
+.text-secondary {
+ color: var(--color-secondary);
+}
diff --git a/src/frontend/src/lib/styles/global/theme.scss b/src/frontend/src/lib/styles/global/theme.scss
index 727f57ab0..0176e4734 100644
--- a/src/frontend/src/lib/styles/global/theme.scss
+++ b/src/frontend/src/lib/styles/global/theme.scss
@@ -112,17 +112,23 @@ figcaption {
}
::selection {
- background: var(--color-misty-rose);
- color: var(--color-off-white);
+ background: var(--color-pale-cornflower-blue);
}
::-moz-selection {
- background: var(--color-misty-rose);
- color: var(--color-off-white);
+ background: var(--color-pale-cornflower-blue);
}
+// TODO: set the scrollbar style for Firefox too. Right now it is not ideal since the modifiable properties are limited and they go in conflict with what we set (https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_scrollbars_styling)
::-webkit-scrollbar-thumb {
- background-color: var(--color-misty-rose);
+ border: 1px solid rgba(var(--color-white-rgb), 0.4);
+ background: rgba(var(--color-black-rgb), 0.2);
+ border-radius: 16px;
+
+ &:hover {
+ border-color: rgba(var(--color-white-rgb), 0.7);
+ background: rgba(var(--color-black-rgb), 0.3);
+ }
}
::-webkit-scrollbar-track {
diff --git a/src/frontend/src/lib/styles/global/variables.scss b/src/frontend/src/lib/styles/global/variables.scss
index ea021f47f..a1b2e994c 100644
--- a/src/frontend/src/lib/styles/global/variables.scss
+++ b/src/frontend/src/lib/styles/global/variables.scss
@@ -8,6 +8,7 @@
--padding-1_25x: calc(var(--padding) * 1.25);
--padding-1_5x: calc(var(--padding) * 1.5);
--padding-2x: calc(var(--padding) * 2);
+ --padding-2_5x: calc(var(--padding) * 2.5);
--padding-3x: calc(var(--padding) * 3);
--padding-4x: calc(var(--padding) * 4);
--padding-6x: calc(var(--padding) * 6);
@@ -18,6 +19,5 @@
--border-radius-md: calc(var(--border-radius-sm) * 2);
--border-radius-lg: calc(var(--border-radius-sm) * 4);
- // Overlays - based on --color-dark
- --backdrop: rgba(14, 0, 45, 0.6);
+ --backdrop: rgba(var(--color-secondary-rgb), 0.6);
}
diff --git a/src/frontend/src/lib/types/api.ts b/src/frontend/src/lib/types/api.ts
new file mode 100644
index 000000000..08f3a7a2d
--- /dev/null
+++ b/src/frontend/src/lib/types/api.ts
@@ -0,0 +1,17 @@
+import type {
+ AddUserCredentialError,
+ CredentialSpec,
+ GetUserProfileError,
+ UserProfile
+} from '$declarations/backend/backend.did';
+import { Principal } from '@dfinity/principal';
+
+export interface AddUserCredentialParams {
+ credentialJwt: string;
+ issuerCanisterId: Principal;
+ currentUserVersion?: bigint;
+ credentialSpec: CredentialSpec;
+}
+export type AddUserCredentialResponse = { Ok: null } | { Err: AddUserCredentialError };
+
+export type GetUserProfileResponse = { Ok: UserProfile } | { Err: GetUserProfileError };
diff --git a/src/frontend/src/lib/types/balance.ts b/src/frontend/src/lib/types/balance.ts
index 8e7582272..8983ddb4d 100644
--- a/src/frontend/src/lib/types/balance.ts
+++ b/src/frontend/src/lib/types/balance.ts
@@ -1,5 +1,5 @@
+import type { Option } from '$lib/types/utils';
import { BigNumber } from '@ethersproject/bignumber';
-import type { Option } from './utils';
export type Balance = BigNumber;
diff --git a/src/frontend/src/lib/types/canister.ts b/src/frontend/src/lib/types/canister.ts
index 9e5c2085f..48ad651bc 100644
--- a/src/frontend/src/lib/types/canister.ts
+++ b/src/frontend/src/lib/types/canister.ts
@@ -1,5 +1,20 @@
+import type { OptionIdentity } from '$lib/types/identity';
import type { Option } from '$lib/types/utils';
+import type { Identity } from '@dfinity/agent';
+import { Principal } from '@dfinity/principal';
+import type { CanisterOptions } from '@dfinity/utils';
export type CanisterIdText = string;
export type OptionCanisterIdText = Option;
+
+export type CanisterApiFunctionParams = T & {
+ nullishIdentityErrorMessage?: string;
+ identity: OptionIdentity;
+ canisterId?: CanisterIdText;
+};
+
+export interface CreateCanisterOptions extends Omit, 'canisterId' | 'agent'> {
+ canisterId: Principal;
+ identity: Identity;
+}
diff --git a/src/frontend/src/lib/types/i18n.d.ts b/src/frontend/src/lib/types/i18n.d.ts
index 579633994..f86b55173 100644
--- a/src/frontend/src/lib/types/i18n.d.ts
+++ b/src/frontend/src/lib/types/i18n.d.ts
@@ -17,9 +17,11 @@ interface I18nCore {
amount: string;
max: string;
more: string;
+ reject: string;
+ approve: string;
};
info: { test_banner: string };
- alt: { logo: string; background: string; go_to_home: string };
+ alt: { logo: string; go_to_home: string };
}
interface I18nNavigation {
@@ -27,10 +29,10 @@ interface I18nNavigation {
source_code_on_github: string;
view_on_explorer: string;
source_code: string;
- oisy_roadmap: string;
+ changelog: string;
submit_ticket: string;
};
- alt: { more_settings: string; menu: string; oisy_roadmap: string; submit_ticket: string };
+ alt: { more_settings: string; menu: string; changelog: string; submit_ticket: string };
}
interface I18nAuth {
@@ -396,8 +398,6 @@ interface I18nWallet_connect {
text: {
name: string;
session_proposal: string;
- approve: string;
- reject: string;
connect: string;
connecting: string;
disconnect: string;
diff --git a/src/frontend/src/lib/types/network.ts b/src/frontend/src/lib/types/network.ts
index 1ac6b944f..cc4d8da35 100644
--- a/src/frontend/src/lib/types/network.ts
+++ b/src/frontend/src/lib/types/network.ts
@@ -1,3 +1,6 @@
+import type { OnramperNetworkId } from '$lib/types/onramper';
+import type { AtLeastOne } from '$lib/types/utils';
+
export type NetworkId = symbol;
export type NetworkEnvironment = 'mainnet' | 'testnet';
@@ -7,4 +10,9 @@ export interface Network {
env: NetworkEnvironment;
name: string;
icon?: string;
+ buy?: AtLeastOne;
+}
+
+export interface NetworkBuy {
+ onramperId?: OnramperNetworkId;
}
diff --git a/src/frontend/src/lib/types/onramper.ts b/src/frontend/src/lib/types/onramper.ts
new file mode 100644
index 000000000..8b00856a8
--- /dev/null
+++ b/src/frontend/src/lib/types/onramper.ts
@@ -0,0 +1,7 @@
+// The list of networks that are supported by Onramper can be found here:
+// https://docs.onramper.com/docs/network-support
+export type OnramperNetworkId = 'icp' | 'bitcoin' | 'ethereum';
+
+// The list of cryptocurrencies that are supported by Onramper can be found here:
+// https://docs.onramper.com/docs/crypto-asset-support
+export type OnramperId = string;
diff --git a/src/frontend/src/lib/types/post-message.ts b/src/frontend/src/lib/types/post-message.ts
index 88014e114..46fffe37d 100644
--- a/src/frontend/src/lib/types/post-message.ts
+++ b/src/frontend/src/lib/types/post-message.ts
@@ -1,15 +1,16 @@
import type { Erc20ContractAddress } from '$eth/types/erc20';
-import type { PostMessageWalletData } from '$icp/types/ic.post-message';
import type {
CoingeckoSimplePriceResponse,
CoingeckoSimpleTokenPriceResponse
} from '$lib/types/coingecko';
+import type { BitcoinNetwork as SignerBitcoinNetwork } from '$declarations/signer/signer.did';
import type { BtcAddressData } from '$icp/stores/btc.store';
import type { JsonText } from '$icp/types/btc.post-message';
import type { LedgerCanisterIdText } from '$icp/types/canister';
import type { IcCanisters, IcCkMetadata } from '$icp/types/ic';
import type { Network } from '$lib/types/network';
+import type { CertifiedData } from '$lib/types/store';
import type { SyncState } from '$lib/types/sync';
import type { BitcoinNetwork } from '@dfinity/ckbtc';
@@ -26,6 +27,9 @@ export type PostMessageRequest =
| 'stopIcrcWalletTimer'
| 'startIcrcWalletTimer'
| 'triggerIcrcWalletTimer'
+ | 'stopBtcWalletTimer'
+ | 'startBtcWalletTimer'
+ | 'triggerBtcWalletTimer'
| 'stopBtcStatusesTimer'
| 'startBtcStatusesTimer'
| 'triggerBtcStatusesTimer'
@@ -52,8 +56,13 @@ export type PostMessageDataRequestIcCkBTCUpdateBalance = PostMessageDataRequestI
bitcoinNetwork: BitcoinNetwork;
};
+export interface PostMessageDataRequestBtc {
+ bitcoinNetwork: SignerBitcoinNetwork;
+}
+
export type PostMessageResponseStatus =
- | 'syncWalletStatus'
+ | 'syncIcWalletStatus'
+ | 'syncBtcWalletStatus'
| 'syncBtcStatusesStatus'
| 'syncCkMinterInfoStatus'
| 'syncCkBTCUpdateBalanceStatus';
@@ -65,6 +74,7 @@ export type PostMessageResponse =
| 'syncExchangeError'
| 'syncIcpWallet'
| 'syncIcrcWallet'
+ | 'syncBtcWallet'
| 'syncIcpWalletError'
| 'syncIcrcWalletError'
| 'syncIcpWalletCleanUp'
@@ -94,10 +104,23 @@ export interface PostMessageDataResponseExchangeError extends PostMessageDataRes
err: string | undefined;
}
+// Transactions & {certified: boolean}
+type JsonTransactionsText = string;
+
+type PostMessageWalletData = Omit & {
+ balance: CertifiedData;
+ newTransactions: JsonTransactionsText;
+};
+
export interface PostMessageDataResponseWallet extends PostMessageDataResponse {
wallet: PostMessageWalletData;
}
+// TODO: use common PostMessageDataResponseWallet interface after BTC transactions added to the worker
+export interface PostMessageDataResponseBtcWallet extends PostMessageDataResponse {
+ wallet: Omit;
+}
+
export interface PostMessageDataResponseError extends PostMessageDataResponse {
error: unknown;
}
diff --git a/src/frontend/src/lib/types/token.ts b/src/frontend/src/lib/types/token.ts
index 434e72777..64b024a1b 100644
--- a/src/frontend/src/lib/types/token.ts
+++ b/src/frontend/src/lib/types/token.ts
@@ -1,6 +1,7 @@
import type { OptionBalance } from '$lib/types/balance';
import type { Network } from '$lib/types/network';
-import type { Option, RequiredExcept } from '$lib/types/utils';
+import type { OnramperId } from '$lib/types/onramper';
+import type { AtLeastOne, Option, RequiredExcept } from '$lib/types/utils';
export type TokenId = symbol;
@@ -14,7 +15,8 @@ export type Token = {
standard: TokenStandard;
category: TokenCategory;
} & TokenMetadata &
- TokenAppearance;
+ TokenAppearance &
+ TokenBuyable;
export interface TokenMetadata {
name: string;
@@ -27,10 +29,18 @@ export interface TokenAppearance {
oisyName?: TokenOisyName;
}
-export type TokenOisyName = {
+export interface TokenOisyName {
prefix: string | undefined;
oisyName: string;
-};
+}
+
+export interface TokenBuyable {
+ buy?: AtLeastOne;
+}
+
+export interface TokenBuy {
+ onramperId?: OnramperId;
+}
export interface TokenLinkedData {
twinTokenSymbol?: string;
@@ -38,7 +48,9 @@ export interface TokenLinkedData {
export type TokenWithLinkedData = Token & TokenLinkedData;
-export type RequiredToken = RequiredExcept;
+export type NonRequiredProps = TokenAppearance & TokenBuyable;
+
+export type RequiredToken = RequiredExcept;
export type RequiredTokenWithLinkedData = RequiredToken;
@@ -54,5 +66,3 @@ interface TokenFinancialData {
}
export type TokenUi = Token & TokenFinancialData;
-
-export type TokenIndexKey = string;
diff --git a/src/frontend/src/lib/types/utils.ts b/src/frontend/src/lib/types/utils.ts
index 260969c8c..6377683d7 100644
--- a/src/frontend/src/lib/types/utils.ts
+++ b/src/frontend/src/lib/types/utils.ts
@@ -1,9 +1,15 @@
export type RequiredExcept = Required> & Pick;
-export type ResultSuccess = { success: boolean; err?: T };
+export interface ResultSuccess {
+ success: boolean;
+ err?: T;
+}
export type ResultSuccessReduced = ResultSuccess;
// We disable the eslint rule here because this is the utility type that we use to define such rule.
// eslint-disable-next-line local-rules/use-option-type-wrapper
export type Option = T | null | undefined;
+
+export type AtLeastOne = Pick> &
+ { [K in Keys]-?: Required> & Partial> }[Keys];
diff --git a/src/frontend/src/lib/utils/i18n.utils.ts b/src/frontend/src/lib/utils/i18n.utils.ts
index 8bfcfed35..b3c08150c 100644
--- a/src/frontend/src/lib/utils/i18n.utils.ts
+++ b/src/frontend/src/lib/utils/i18n.utils.ts
@@ -12,7 +12,9 @@ import { isNullish, nonNullish } from '@dfinity/utils';
const escapeRegExp = (regExpText: string): string =>
regExpText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
-export type I18nSubstitutions = { [from: string]: string };
+export interface I18nSubstitutions {
+ [from: string]: string;
+}
/**
* @example
diff --git a/src/frontend/src/lib/utils/nav.utils.ts b/src/frontend/src/lib/utils/nav.utils.ts
index c08b02c8a..4e1b0ab8f 100644
--- a/src/frontend/src/lib/utils/nav.utils.ts
+++ b/src/frontend/src/lib/utils/nav.utils.ts
@@ -50,12 +50,12 @@ export const gotoReplaceRoot = async () => {
await goto('/', { replaceState: true });
};
-export type RouteParams = {
+export interface RouteParams {
token: OptionString;
network: OptionString;
// WalletConnect URI parameter
uri: OptionString;
-};
+}
export const loadRouteParams = ($event: LoadEvent): RouteParams => {
if (!browser) {
diff --git a/src/frontend/src/lib/utils/network.utils.ts b/src/frontend/src/lib/utils/network.utils.ts
index 68d39c5b7..88b40d376 100644
--- a/src/frontend/src/lib/utils/network.utils.ts
+++ b/src/frontend/src/lib/utils/network.utils.ts
@@ -1,3 +1,4 @@
+import type { BitcoinNetwork as SignerBitcoinNetwork } from '$declarations/signer/signer.did';
import {
BITCOIN_NETWORKS_IDS,
ICP_NETWORK_ID,
@@ -6,6 +7,7 @@ import {
import { isTokenIcrcTestnet } from '$icp/utils/icrc-ledger.utils';
import type { Network, NetworkId } from '$lib/types/network';
import type { Token } from '$lib/types/token';
+import type { BitcoinNetwork } from '@dfinity/ckbtc';
import { nonNullish } from '@dfinity/utils';
export const isNetworkICP = (network: Network | undefined): boolean => isNetworkIdICP(network?.id);
@@ -37,3 +39,10 @@ export const filterTokensForSelectedNetwork = ([
$selectedNetwork?.id === networkId
);
});
+
+export const mapToSignerBitcoinNetwork = ({
+ network
+}: {
+ network: BitcoinNetwork;
+}): SignerBitcoinNetwork =>
+ ({ mainnet: { mainnet: null }, testnet: { testnet: null }, regtest: { regtest: null } })[network];
diff --git a/src/frontend/src/routes/(app)/+layout.svelte b/src/frontend/src/routes/(app)/+layout.svelte
index 7f3b5c88a..71e7b37ec 100644
--- a/src/frontend/src/routes/(app)/+layout.svelte
+++ b/src/frontend/src/routes/(app)/+layout.svelte
@@ -1,6 +1,5 @@
-
-
{
+ const actual = await importOriginal();
+ return {
+ ...actual,
+ LOCAL: false
+ };
+});
+
+describe('backend.canister', () => {
+ const createBackendCanister = async ({
+ serviceOverride
+ }: Pick, 'serviceOverride'>): Promise =>
+ BackendCanister.create({
+ canisterId: Principal.fromText('tdxud-2yaaa-aaaad-aadiq-cai'),
+ identity: mockIdentity,
+ certifiedServiceOverride: serviceOverride,
+ serviceOverride
+ });
+ const service = mock>();
+ const mockResponseError = new Error('Test response error');
+ const queryParams = {
+ certified: false
+ };
+
+ const addUserCredentialParams = {
+ credentialJwt: 'test-credential-jwt',
+ issuerCanisterId: mockPrincipal,
+ currentUserVersion: 0n,
+ credentialSpec: {
+ arguments: [],
+ credential_type: ''
+ }
+ } as AddUserCredentialParams;
+ const addUserCredentialEndpointParams = {
+ credential_jwt: addUserCredentialParams.credentialJwt,
+ issuer_canister_id: addUserCredentialParams.issuerCanisterId,
+ current_user_version: toNullable(addUserCredentialParams.currentUserVersion),
+ credential_spec: addUserCredentialParams.credentialSpec
+ };
+
+ const mockedUserProfile = {
+ credentials: [
+ {
+ issuer: 'test-issuer',
+ verified_date_timestamp: [],
+ credential_type: { ProofOfUniqueness: null }
+ }
+ ],
+ version: [],
+ created_timestamp: 1n,
+ updated_timestamp: 1n
+ } as UserProfile;
+
+ const mockedUserToken = {
+ decimals: [],
+ version: [],
+ enabled: [],
+ chain_id: 1n,
+ contract_address: 'test_address',
+ symbol: []
+ } as UserToken;
+ const userTokens = [mockedUserToken];
+
+ const mockedCustomToken = {
+ token: {
+ Icrc: {
+ ledger_id: mockPrincipal,
+ index_id: []
+ } as IcrcToken
+ },
+ version: [],
+ enabled: false
+ } as CustomToken;
+ const customTokens = [mockedCustomToken];
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('returns user tokens', async () => {
+ service.list_user_tokens.mockResolvedValue(userTokens);
+
+ const { listUserTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await listUserTokens(queryParams);
+
+ expect(res).toEqual(userTokens);
+ });
+
+ it('should throw an error if list_user_tokens throws', async () => {
+ service.list_user_tokens.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { listUserTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = listUserTokens(queryParams);
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('returns custom tokens', async () => {
+ service.list_custom_tokens.mockResolvedValue(customTokens);
+
+ const { listCustomTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await listCustomTokens(queryParams);
+
+ expect(res).toEqual(customTokens);
+ });
+
+ it('should throw an error if list_custom_tokens throws', async () => {
+ service.list_custom_tokens.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { listCustomTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = listCustomTokens(queryParams);
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('sets many custom tokens', async () => {
+ const { setManyCustomTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await setManyCustomTokens({ tokens: customTokens });
+
+ expect(service.set_many_custom_tokens).toHaveBeenCalledWith(customTokens);
+ expect(res).toEqual(undefined);
+ });
+
+ it('should throw an error if set_many_custom_tokens throws', async () => {
+ service.set_many_custom_tokens.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { setManyCustomTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = setManyCustomTokens({ tokens: customTokens });
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('sets custom token', async () => {
+ const { setCustomToken } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await setCustomToken({ token: mockedCustomToken });
+
+ expect(service.set_custom_token).toHaveBeenCalledWith(mockedCustomToken);
+ expect(res).toEqual(undefined);
+ });
+
+ it('should throw an error if set_custom_token throws', async () => {
+ service.set_custom_token.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { setCustomToken } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = setCustomToken({ token: mockedCustomToken });
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('sets many user tokens', async () => {
+ const { setManyUserTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await setManyUserTokens({ tokens: userTokens });
+
+ expect(service.set_many_user_tokens).toHaveBeenCalledWith(userTokens);
+ expect(res).toEqual(undefined);
+ });
+
+ it('should throw an error if set_many_user_tokens throws', async () => {
+ service.set_many_user_tokens.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { setManyUserTokens } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = setManyUserTokens({ tokens: userTokens });
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('sets user token', async () => {
+ const { setUserToken } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await setUserToken({ token: mockedUserToken });
+
+ expect(service.set_user_token).toHaveBeenCalledWith(mockedUserToken);
+ expect(res).toEqual(undefined);
+ });
+
+ it('should throw an error if set_user_token throws', async () => {
+ service.set_user_token.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { setUserToken } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = setUserToken({ token: mockedUserToken });
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('creates user profile', async () => {
+ service.create_user_profile.mockResolvedValue(mockedUserProfile);
+
+ const { createUserProfile } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await createUserProfile();
+
+ expect(res).toEqual(mockedUserProfile);
+ });
+
+ it('should throw an error if create_user_profile throws', async () => {
+ service.create_user_profile.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { createUserProfile } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = createUserProfile();
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('returns user profile success response', async () => {
+ const response = { Ok: mockedUserProfile };
+ service.get_user_profile.mockResolvedValue(response);
+
+ const { getUserProfile } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await getUserProfile(queryParams);
+
+ expect(res).toEqual(response);
+ });
+
+ it('returns user profile error response', async () => {
+ const response = { Err: { NotFound: null } };
+ service.get_user_profile.mockResolvedValue({ Err: { NotFound: null } });
+
+ const { getUserProfile } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await getUserProfile(queryParams);
+
+ expect(res).toEqual(response);
+ });
+
+ it('should throw an error if get_user_profile throws', async () => {
+ service.get_user_profile.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { getUserProfile } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = getUserProfile(queryParams);
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+
+ it('adds user credentials with success response', async () => {
+ const response = { Ok: null };
+
+ service.add_user_credential.mockResolvedValue(response);
+
+ const { addUserCredential } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await addUserCredential(addUserCredentialParams);
+
+ expect(service.add_user_credential).toHaveBeenCalledWith(addUserCredentialEndpointParams);
+ expect(res).toEqual(response);
+ });
+
+ it('adds user credentials with error response', async () => {
+ const response = { Err: { InvalidCredential: null } };
+ service.add_user_credential.mockResolvedValue(response);
+
+ const { addUserCredential } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = await addUserCredential(addUserCredentialParams);
+
+ expect(service.add_user_credential).toHaveBeenCalledWith(addUserCredentialEndpointParams);
+ expect(res).toEqual(response);
+ });
+
+ it('should throw an error if add_user_credential throws', async () => {
+ service.add_user_credential.mockImplementation(async () => {
+ throw mockResponseError;
+ });
+
+ const { addUserCredential } = await createBackendCanister({
+ serviceOverride: service
+ });
+
+ const res = addUserCredential(addUserCredentialParams);
+
+ await expect(res).rejects.toThrow(mockResponseError);
+ });
+});
diff --git a/src/frontend/src/tests/lib/canisters/signer.canister.spec.ts b/src/frontend/src/tests/lib/canisters/signer.canister.spec.ts
index 30649cb8d..c942df3a9 100644
--- a/src/frontend/src/tests/lib/canisters/signer.canister.spec.ts
+++ b/src/frontend/src/tests/lib/canisters/signer.canister.spec.ts
@@ -1,5 +1,6 @@
import type { _SERVICE as SignerService, SignRequest } from '$declarations/signer/signer.did';
-import { SignerCanister, type SignerCanisterOptions } from '$lib/canisters/signer.canister';
+import { SignerCanister } from '$lib/canisters/signer.canister';
+import type { CreateCanisterOptions } from '$lib/types/canister';
import { type ActorSubclass } from '@dfinity/agent';
import { Principal } from '@dfinity/principal';
import { mock } from 'vitest-mock-extended';
@@ -16,7 +17,7 @@ vi.mock(import('$lib/constants/app.constants'), async (importOriginal) => {
describe('signer.canister', () => {
const createSignerCanister = async ({
serviceOverride
- }: Pick): Promise =>
+ }: Pick, 'serviceOverride'>): Promise =>
SignerCanister.create({
canisterId: Principal.fromText('tdxud-2yaaa-aaaad-aadiq-cai'),
identity: mockIdentity,
@@ -52,8 +53,9 @@ describe('signer.canister', () => {
});
it('returns correct BTC address', async () => {
- const response = 'test-bitcoin-address';
- service.caller_btc_address.mockResolvedValue(response);
+ const address = 'test-bitcoin-address';
+ const response = { Ok: { address } };
+ service.btc_caller_address.mockResolvedValue(response);
const { getBtcAddress } = await createSignerCanister({
serviceOverride: service
@@ -61,12 +63,28 @@ describe('signer.canister', () => {
const res = await getBtcAddress(btcParams);
- expect(res).toEqual(response);
- expect(service.caller_btc_address).toHaveBeenCalledWith(btcParams.network);
+ expect(res).toEqual(address);
+ expect(service.btc_caller_address).toHaveBeenCalledWith(
+ { network: btcParams.network, address_type: { P2WPKH: null } },
+ []
+ );
+ });
+
+ it('should throw an error if btc_caller_address returns an error', async () => {
+ const response = { Err: { InternalError: { msg: 'Test error' } } };
+ service.btc_caller_address.mockResolvedValue(response);
+
+ const { getBtcAddress } = await createSignerCanister({
+ serviceOverride: service
+ });
+
+ const res = getBtcAddress(btcParams);
+
+ await expect(res).rejects.toThrow(new Error("Couldn't get BTC address"));
});
- it('should throw an error if caller_btc_address throws', async () => {
- service.caller_btc_address.mockImplementation(async () => {
+ it('should throw an error if btc_caller_address throws', async () => {
+ service.btc_caller_address.mockImplementation(async () => {
throw mockResponseError;
});
@@ -80,8 +98,9 @@ describe('signer.canister', () => {
});
it('returns correct BTC balance', async () => {
- const response = 2n;
- service.caller_btc_balance.mockResolvedValue(response);
+ const balance = 2n;
+ const response = { Ok: { balance } };
+ service.btc_caller_balance.mockResolvedValue(response);
const { getBtcBalance } = await createSignerCanister({
serviceOverride: service
@@ -89,12 +108,28 @@ describe('signer.canister', () => {
const res = await getBtcBalance(btcParams);
- expect(res).toEqual(response);
- expect(service.caller_btc_balance).toHaveBeenCalledWith(btcParams.network);
+ expect(res).toEqual(balance);
+ expect(service.btc_caller_balance).toHaveBeenCalledWith(
+ { network: btcParams.network, address_type: { P2WPKH: null } },
+ []
+ );
+ });
+
+ it('should throw an error if btc_caller_balance returns an error', async () => {
+ const response = { Err: { InternalError: { msg: 'Test error' } } };
+ service.btc_caller_balance.mockResolvedValue(response);
+
+ const { getBtcBalance } = await createSignerCanister({
+ serviceOverride: service
+ });
+
+ const res = getBtcBalance(btcParams);
+
+ await expect(res).rejects.toThrow(new Error("Couldn't get BTC balance"));
});
- it('should throw an error if caller_btc_balance throws', async () => {
- service.caller_btc_balance.mockImplementation(async () => {
+ it('should throw an error if btc_caller_balance throws', async () => {
+ service.btc_caller_balance.mockImplementation(async () => {
throw mockResponseError;
});
diff --git a/src/frontend/src/tests/lib/services/load-user-profile.spec.ts b/src/frontend/src/tests/lib/services/load-user-profile.spec.ts
index 791adccdb..46eed0c78 100644
--- a/src/frontend/src/tests/lib/services/load-user-profile.spec.ts
+++ b/src/frontend/src/tests/lib/services/load-user-profile.spec.ts
@@ -5,6 +5,7 @@ import { userProfileStore } from '$lib/stores/user-profile.store';
import { waitFor } from '@testing-library/svelte';
import { beforeEach } from 'node:test';
import { get } from 'svelte/store';
+import en from '../../mocks/i18n.mock';
import { mockIdentity } from '../../mocks/identity.mock';
vi.mock('$lib/api/backend.api');
@@ -15,6 +16,7 @@ const mockProfile: UserProfile = {
created_timestamp: 1234n,
updated_timestamp: 1234n
};
+const nullishIdentityErrorMessage = en.auth.error.no_internet_identity;
describe('loadUserProfile', () => {
beforeEach(() => {
@@ -32,7 +34,8 @@ describe('loadUserProfile', () => {
expect(getUserProfileSpy).toHaveBeenCalledWith({
identity: mockIdentity,
- certified: false
+ certified: false,
+ nullishIdentityErrorMessage
});
expect(createUserProfileSpy).not.toHaveBeenCalled();
expect(get(userProfileStore)).toEqual({ certified: false, profile: mockProfile });
@@ -50,10 +53,12 @@ describe('loadUserProfile', () => {
expect(getUserProfileSpy).toHaveBeenCalledWith({
identity: mockIdentity,
- certified: false
+ certified: false,
+ nullishIdentityErrorMessage
});
expect(createUserProfileSpy).toHaveBeenCalledWith({
- identity: mockIdentity
+ identity: mockIdentity,
+ nullishIdentityErrorMessage
});
expect(get(userProfileStore)).toEqual({ certified: true, profile: mockProfile });
});
@@ -67,11 +72,13 @@ describe('loadUserProfile', () => {
expect(getUserProfileSpy).toHaveBeenCalledWith({
identity: mockIdentity,
- certified: false
+ certified: false,
+ nullishIdentityErrorMessage
});
expect(getUserProfileSpy).toHaveBeenCalledWith({
identity: mockIdentity,
- certified: true
+ certified: true,
+ nullishIdentityErrorMessage
});
await waitFor(() =>
expect(get(userProfileStore)).toEqual({ certified: true, profile: mockProfile })
diff --git a/src/frontend/src/tests/lib/utils/certified-store.utils.spec.ts b/src/frontend/src/tests/lib/utils/certified-store.utils.spec.ts
index f6e528abd..62665189e 100644
--- a/src/frontend/src/tests/lib/utils/certified-store.utils.spec.ts
+++ b/src/frontend/src/tests/lib/utils/certified-store.utils.spec.ts
@@ -31,7 +31,10 @@ describe('mapCertifiedData', () => {
});
it('should return the data when certifiedData contains an object', () => {
- type TestData = { prop: string; value: number };
+ interface TestData {
+ prop: string;
+ value: number;
+ }
const data: TestData = { prop: 'testData', value: 1 };
const certifiedData: Option> = { data, certified };
diff --git a/src/frontend/src/tests/mocks/identity.mock.ts b/src/frontend/src/tests/mocks/identity.mock.ts
index f0a56de5e..a0e3eea64 100644
--- a/src/frontend/src/tests/mocks/identity.mock.ts
+++ b/src/frontend/src/tests/mocks/identity.mock.ts
@@ -3,7 +3,7 @@ import { Principal } from '@dfinity/principal';
const mockPrincipalText = 'xlmdg-vkosz-ceopx-7wtgu-g3xmd-koiyc-awqaq-7modz-zf6r6-364rh-oqe';
-const mockPrincipal = Principal.fromText(mockPrincipalText);
+export const mockPrincipal = Principal.fromText(mockPrincipalText);
export const mockIdentity = {
getPrincipal: () => mockPrincipal
diff --git a/src/frontend/static/manifest.webmanifest b/src/frontend/static/manifest.webmanifest
index e9647ed69..453284906 100644
--- a/src/frontend/static/manifest.webmanifest
+++ b/src/frontend/static/manifest.webmanifest
@@ -3,8 +3,8 @@
"short_name": "{{OISY_NAME}}",
"start_url": ".",
"display": "standalone",
- "theme_color": "#016DFC",
- "background_color": "#016DFC",
+ "theme_color": "#FFFFFF",
+ "background_color": "#FFFFFF",
"description": "{{OISY_ONELINER}}",
"orientation": "portrait-primary",
"prefer_related_applications": false,
diff --git a/tailwind.config.js b/tailwind.config.js
index 7ba0a1814..1f9c6092c 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -11,18 +11,19 @@ export default {
inherit: 'inherit',
transparent: 'transparent',
current: 'currentColor',
- black: 'rgb(0 0 0)',
+ 'black-rgb': '0, 0, 0',
white: 'rgb(255 255 255)',
+ 'white-rgb': '255, 255, 255',
'off-white': '#fcfaf6',
dust: '#dbd9d6',
grey: '#c0bbc4',
'light-blue': '#ede7fb',
blue: '#3b00b9',
'blue-ribbon-rgb': '0, 102, 255',
- dark: '#0e002d',
'dark-blue': '#321469',
'brandeis-blue': '#016dfc',
'cetacean-blue': '#0e002d',
+ 'pale-cornflower-blue': '#b0cdff',
'brilliant-azure': '#348afd',
'misty-rose': '#937993',
goldenrod: '#dfa81b',
@@ -31,7 +32,12 @@ export default {
'alice-blue': '#ecf3fb',
'american-orange': '#ff8a00',
'crayola-yellow': '#ffe57f',
- cornsilk: '#fff7d8'
+ cornsilk: '#fff7d8',
+ cobalt: '#004abe',
+ 'resolution-blue': '#012f80',
+ zumthor: '#e8f1ff',
+ onahau: '#d1e3ff',
+ anakiwa: '#b0cdff'
},
extend: {
minWidth: {
diff --git a/vite.config.ts b/vite.config.ts
index c475bed77..16493c84e 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,6 @@
import inject from '@rollup/plugin-inject';
import { sveltekit } from '@sveltejs/kit/vite';
-import { dirname, resolve } from 'node:path';
+import { basename, dirname, resolve } from 'node:path';
import { defineConfig, loadEnv, type UserConfig } from 'vite';
import { defineViteReplacements, readCanisterIds } from './vite.utils';
@@ -59,7 +59,12 @@ const config: UserConfig = {
inject({
modules: { Buffer: ['buffer', 'Buffer'] }
})
- ]
+ ],
+ external: (id) => {
+ // A list of file to exclude because we parse those manually with custom scripts.
+ const filename = basename(id);
+ return ['+oisy.page.css'].includes(filename);
+ }
}
},
// proxy /api to port 4943 during development
@@ -69,7 +74,6 @@ const config: UserConfig = {
}
},
optimizeDeps: {
- include: ['three'],
esbuildOptions: {
define: {
global: 'globalThis'
diff --git a/vite.utils.ts b/vite.utils.ts
index 25747848b..4a54802c1 100644
--- a/vite.utils.ts
+++ b/vite.utils.ts
@@ -33,20 +33,20 @@ const readRemoteCanisterIds = ({ prefix }: { prefix?: string }): Record;
- };
+ }
const { canisters }: DfxJson = JSON.parse(readFileSync(dfxJsonFile, 'utf8'));