From b3d06e7f9e328e36e030c0a6f1d99242ee214150 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 6 Aug 2024 20:39:24 +0300 Subject: [PATCH 01/24] allow only patch version updates for @solana/actions-spec --- bun.lockb | Bin 386919 -> 386967 bytes package.json | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/bun.lockb b/bun.lockb index e9ff3b77ee71bf8e0dcbc2bbcda94853519ec6a0..e96daddb4b44f0b9e2a4207ddb3cc2f1ba453625 100755 GIT binary patch delta 8837 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 delta 8781 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 diff --git a/package.json b/package.json index c43f9098..370b64e7 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "dist" ], "devDependencies": { - "@solana/actions-spec": "^2.2.0", + "@solana/actions-spec": "~2.2.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@typescript-eslint/eslint-plugin": "^7.16.1", From 712c9d1d06e95fb0f63b203483405194a20a5e11 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 6 Aug 2024 20:39:56 +0300 Subject: [PATCH 02/24] add util to extract @solana/actions-spec version --- src/utils/dependency-versions.ts | 9 +++++++++ test/utils/dependency-versions.spec.ts | 10 ++++++++++ 2 files changed, 19 insertions(+) create mode 100644 src/utils/dependency-versions.ts create mode 100644 test/utils/dependency-versions.spec.ts diff --git a/src/utils/dependency-versions.ts b/src/utils/dependency-versions.ts new file mode 100644 index 00000000..c75f1ebe --- /dev/null +++ b/src/utils/dependency-versions.ts @@ -0,0 +1,9 @@ +import packageJson from '../../package.json'; + +const pkg = packageJson as unknown as { + devDependencies: { '@solana/actions-spec': string }; +}; + +export const ACTIONS_SPEC_VERSION = pkg.devDependencies[ + '@solana/actions-spec' +].replace(/[^\d.]/g, ''); diff --git a/test/utils/dependency-versions.spec.ts b/test/utils/dependency-versions.spec.ts new file mode 100644 index 00000000..98e781ed --- /dev/null +++ b/test/utils/dependency-versions.spec.ts @@ -0,0 +1,10 @@ +import { describe, expect, test } from 'bun:test'; +import { ACTIONS_SPEC_VERSION } from '../../src/utils/dependency-versions.ts'; + +describe('dependencyVersions', () => { + test('should extract the correct version numbe for actions spec', () => { + expect(ACTIONS_SPEC_VERSION).not.toBeNull(); + expect(ACTIONS_SPEC_VERSION).not.toBeUndefined(); + expect(ACTIONS_SPEC_VERSION).toMatch(/^\d+\.\d+\.\d+$/); + }); +}); From 34e041397304480b875d8949ad4a830bdf93e719 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 6 Aug 2024 20:41:14 +0300 Subject: [PATCH 03/24] add todo in dependency version to replace it --- src/utils/dependency-versions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/dependency-versions.ts b/src/utils/dependency-versions.ts index c75f1ebe..31bc6cb0 100644 --- a/src/utils/dependency-versions.ts +++ b/src/utils/dependency-versions.ts @@ -4,6 +4,7 @@ const pkg = packageJson as unknown as { devDependencies: { '@solana/actions-spec': string }; }; +// TODO: to be replaced with the actual version number exported from the actions-spec package export const ACTIONS_SPEC_VERSION = pkg.devDependencies[ '@solana/actions-spec' ].replace(/[^\d.]/g, ''); From 7bbf3b3a82633978fa6a99a6e5820aaa8e38e24d Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 6 Aug 2024 20:57:56 +0300 Subject: [PATCH 04/24] expose version in action metadata, make blockchain ids optional --- src/api/Action/Action.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index 57653a43..8b764f01 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -16,7 +16,8 @@ import { const MULTI_VALUE_TYPES: ActionParameterType[] = ['checkbox']; interface ActionMetadata { - blockchainIds: string[]; + blockchainIds?: string[]; + version?: string; } export class Action { @@ -117,13 +118,16 @@ export class Action { const data = (await response.json()) as ActionGetResponse; - // for multi-chain x-blockchain-ids - const blockchainIds = ( - response?.headers?.get('x-blockchain-ids') || '' - ).split(','); + const blockchainIds = + response.headers + .get('x-blockchain-ids') + ?.split(',') + .map((id) => id.trim()) ?? []; + const version = response.headers.get('x-action-version')?.trim(); const metadata: ActionMetadata = { blockchainIds, + version, }; return new Action(apiUrl, data, metadata, adapter); From 0dd884adfc373e1c66e216c7fd3bf12ecdcab582 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Tue, 6 Aug 2024 20:58:24 +0300 Subject: [PATCH 05/24] add action supportability utils --- src/api/Action/action-supportability.ts | 85 +++++++++ test/api/Action/action-supportability.spec.ts | 162 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 src/api/Action/action-supportability.ts create mode 100644 test/api/Action/action-supportability.spec.ts diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts new file mode 100644 index 00000000..b8a45779 --- /dev/null +++ b/src/api/Action/action-supportability.ts @@ -0,0 +1,85 @@ +import { ACTIONS_SPEC_VERSION } from '../../utils/dependency-versions.ts'; +import type { ActionContext } from '../ActionConfig.ts'; + +/** + * CAIP-2 Blockchain IDs. + */ +export const BlockchainIds = { + SOLANA_MAINNET: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', +}; + +/** + * Max spec version the Blink client supports. + */ +const ACCEPT_ACTION_VERSION = ACTIONS_SPEC_VERSION; +/** + * Baseline action version to be used when not set by action provider. Defaults to latest pre-versioning actions spec release. + */ +const BASELINE_ACTION_VERSION = '1.5.1'; +/** + * Baseline blockchain IDs to be used when not set by action provider. Defaults to Solana mainnet. + */ +const BASELINE_ACTION_BLOCKCHAIN_IDS = [ + BlockchainIds.SOLANA_MAINNET, // Solana mainnet CAIP-2 Blockchain ID +]; + +type IsVersionCompatibleParams = { + actionVersion?: string; + acceptActionVersion?: string; +}; + +export function isVersionSupported({ + acceptActionVersion = ACCEPT_ACTION_VERSION, + actionVersion = BASELINE_ACTION_VERSION, +}: IsVersionCompatibleParams): boolean { + return ( + compareSemverVersionsIgnoringPatch(actionVersion, acceptActionVersion) <= 0 + ); +} + +function compareSemverVersionsIgnoringPatch(v1: string, v2: string): number { + const [major1, minor1] = v1.split('.').map(Number); + const [major2, minor2] = v2.split('.').map(Number); + if (major1 !== major2) { + return major1 - major2; + } else if (minor1 !== minor2) { + return minor1 - minor2; + } + return 0; +} + +type IsBlockchainSupportedParams = { + actionBlockchainIds?: string[]; + acceptBlockchainIds: string[]; +}; + +export function isBlockchainSupported({ + acceptBlockchainIds, + actionBlockchainIds = BASELINE_ACTION_BLOCKCHAIN_IDS, +}: IsBlockchainSupportedParams): boolean { + const sanitizedAcceptBlockchainIds = acceptBlockchainIds.map((it) => + it.trim(), + ); + return actionBlockchainIds.every((chain) => + sanitizedAcceptBlockchainIds.includes(chain), + ); +} + +type CheckSupportedParams = { + acceptBlockchainIds: string[]; +}; + +export function defaultCheckSupported( + context: Omit, + { acceptBlockchainIds }: CheckSupportedParams, +) { + return ( + isBlockchainSupported({ + acceptBlockchainIds, + actionBlockchainIds: context.action.metadata.blockchainIds, + }) && + isVersionSupported({ + actionVersion: context.action.metadata.version, + }) + ); +} diff --git a/test/api/Action/action-supportability.spec.ts b/test/api/Action/action-supportability.spec.ts new file mode 100644 index 00000000..301730f8 --- /dev/null +++ b/test/api/Action/action-supportability.spec.ts @@ -0,0 +1,162 @@ +import { describe, expect, test } from 'bun:test'; +import { + isBlockchainSupported, + isVersionSupported, +} from '../../../src/api/Action/action-supportability.ts'; + +describe('isVersionSupported', () => { + test('returns true when action version is less than client version', () => { + expect( + isVersionSupported({ + actionVersion: '2.1.0', + acceptActionVersion: '2.2.0', + }), + ).toBe(true); + expect( + isVersionSupported({ + actionVersion: '2.2.0', + acceptActionVersion: '2.3.0', + }), + ).toBe(true); + expect( + isVersionSupported({ + actionVersion: '1.0.0', + acceptActionVersion: '2.2.0', + }), + ).toBe(true); + }); + + test('returns true when action version is equal to client version', () => { + expect( + isVersionSupported({ + actionVersion: '2.2.0', + acceptActionVersion: '2.2.0', + }), + ).toBe(true); + }); + + test('returns false when action version is greater than client version', () => { + expect( + isVersionSupported({ + actionVersion: '2.3.0', + acceptActionVersion: '2.2.0', + }), + ).toBe(false); + expect( + isVersionSupported({ + actionVersion: '3.0.0', + acceptActionVersion: '2.2.0', + }), + ).toBe(false); + }); + + test('returns true when action version is not provided and uses baseline version', () => { + expect(isVersionSupported({ acceptActionVersion: '2.2.0' })).toBe(true); + }); + + test('returns true when acceptActionVersion is not provided and uses default', () => { + expect(isVersionSupported({ actionVersion: '2.1.0' })).toBe(true); + expect(isVersionSupported({ actionVersion: '2.2.0' })).toBe(true); + expect(isVersionSupported({ actionVersion: '2.3.0' })).toBe(false); + }); + + test('returns true when both versions are not provided and uses baseline version', () => { + expect(isVersionSupported({})).toBe(true); + }); + + test('returns true when action version has patch version less than client version', () => { + expect( + isVersionSupported({ + actionVersion: '2.2.1', + acceptActionVersion: '2.2.2', + }), + ).toBe(true); + }); + + test('returns false when action version is an incompatible string', () => { + expect( + isVersionSupported({ + actionVersion: 'invalidVersion', + acceptActionVersion: '2.2.0', + }), + ).toBe(false); + }); + + test('returns false when acceptActionVersion is an incompatible string', () => { + expect( + isVersionSupported({ + actionVersion: '2.2.0', + acceptActionVersion: 'invalid.version', + }), + ).toBe(false); + }); + + test('returns false when both versions are incompatible strings', () => { + expect( + isVersionSupported({ + actionVersion: 'invalid.version', + acceptActionVersion: 'invalid.version', + }), + ).toBe(false); + }); +}); + +describe('isBlockchainSupported', () => { + test('returns true when all actionBlockchainIds are supported', () => { + expect( + isBlockchainSupported({ + acceptBlockchainIds: [ + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + 'ethereum:1', + ], + actionBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + }), + ).toBe(true); + }); + + test('returns false when some actionBlockchainIds are not supported', () => { + expect( + isBlockchainSupported({ + acceptBlockchainIds: ['ethereum:1'], + actionBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + }), + ).toBe(false); + }); + + test('returns true when actionBlockchainIds is not provided and uses baseline', () => { + expect( + isBlockchainSupported({ + acceptBlockchainIds: [ + 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + 'ethereum:1', + ], + }), + ).toBe(true); + }); + + test('returns false when actionBlockchainIds is not provided and baseline is not supported', () => { + expect( + isBlockchainSupported({ + acceptBlockchainIds: ['ethereum:1'], + }), + ).toBe(false); + }); + + test('returns true when both blockchainIds and actionBlockchainIds are empty', () => { + expect( + isBlockchainSupported({ + acceptBlockchainIds: [], + actionBlockchainIds: [], + }), + ).toBe(true); + }); + + test('returns false when blockchainIds is empty and actionBlockchainIds is not', () => { + expect( + isBlockchainSupported({ + acceptBlockchainIds: [], + actionBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + }), + ).toBe(false); + }); +}); From b3813b8e3f2f7ead40ed4a15de136157f9480f32 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Wed, 7 Aug 2024 00:14:36 +0300 Subject: [PATCH 06/24] add fallback ui on action not supported --- src/api/index.ts | 1 + src/ext/twitter.tsx | 21 +++++----- src/ui/ActionContainer.tsx | 11 +++-- src/ui/ActionLayout.tsx | 83 ++++++++++++++++++++++++------------- src/ui/Snackbar.tsx | 3 +- src/ui/icons/ConfigIcon.tsx | 21 ++++++++++ 6 files changed, 97 insertions(+), 43 deletions(-) create mode 100644 src/ui/icons/ConfigIcon.tsx diff --git a/src/api/index.ts b/src/api/index.ts index c6f38ab6..a9e3c9c9 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -1,4 +1,5 @@ export * from './Action'; +export * from './Action/action-supportability.ts'; export * from './ActionCallbacks.ts'; export * from './ActionConfig'; export * from './actions-spec'; diff --git a/src/ext/twitter.tsx b/src/ext/twitter.tsx index 4ed66a48..a94300c2 100644 --- a/src/ext/twitter.tsx +++ b/src/ext/twitter.tsx @@ -186,16 +186,13 @@ async function handleNewNode( return; } - if (config.isSupported) { - const supported = await config.isSupported({ - originalUrl: actionUrl.toString(), - action, - actionType: state, - }); - if (!supported) { - return; - } - } + const isSupported = config.isSupported + ? await config.isSupported({ + originalUrl: actionUrl.toString(), + action, + actionType: state, + }) + : true; addMargin(container).replaceChildren( createAction({ @@ -204,6 +201,7 @@ async function handleNewNode( callbacks, options, isInterstitial: interstitialData.isInterstitial, + isSupported, }), ); } @@ -213,12 +211,14 @@ function createAction({ action, callbacks, options, + isSupported, }: { originalUrl: URL; action: Action; callbacks: Partial; options: NormalizedObserverOptions; isInterstitial: boolean; + isSupported: boolean; }) { const container = document.createElement('div'); container.className = 'dialect-action-root-container'; @@ -234,6 +234,7 @@ function createAction({ websiteText={originalUrl.hostname} callbacks={callbacks} securityLevel={options.securityLevel} + isSupported={isSupported} /> , ); diff --git a/src/ui/ActionContainer.tsx b/src/ui/ActionContainer.tsx index 2916351b..b6c6146f 100644 --- a/src/ui/ActionContainer.tsx +++ b/src/ui/ActionContainer.tsx @@ -2,7 +2,10 @@ import { useEffect, useMemo, useReducer, useState } from 'react'; import { AbstractActionComponent, Action, + type ActionCallbacksConfig, + type ActionContext, ButtonActionComponent, + type ExtendedActionState, FormActionComponent, getExtendedActionState, getExtendedInterstitialState, @@ -12,9 +15,6 @@ import { mergeActionStates, MultiValueActionComponent, SingleValueActionComponent, - type ActionCallbacksConfig, - type ActionContext, - type ExtendedActionState, } from '../api'; import { checkSecurity, type SecurityLevel } from '../shared'; import { isInterstitial } from '../utils/interstitial-url.ts'; @@ -24,8 +24,8 @@ import { } from '../utils/type-guards.ts'; import { ActionLayout, - DisclaimerType, type Disclaimer, + DisclaimerType, type StylePreset, } from './ActionLayout'; @@ -205,6 +205,7 @@ export const ActionContainer = ({ securityLevel = DEFAULT_SECURITY_LEVEL, stylePreset = 'default', Experimental__ActionLayout = ActionLayout, + isSupported, }: { action: Action; websiteUrl?: string | null; @@ -212,6 +213,7 @@ export const ActionContainer = ({ callbacks?: Partial; securityLevel?: SecurityLevel | NormalizedSecurityLevel; stylePreset?: StylePreset; + isSupported: boolean; // please do not use it yet, better api is coming.. Experimental__ActionLayout?: typeof ActionLayout; @@ -488,6 +490,7 @@ export const ActionContainer = ({ inputs={inputs.map((input) => asInputProps(input))} form={form ? asFormProps(form) : undefined} disclaimer={disclaimer} + isSupported={isSupported} /> ); }; diff --git a/src/ui/ActionLayout.tsx b/src/ui/ActionLayout.tsx index 48b17c71..333b5b13 100644 --- a/src/ui/ActionLayout.tsx +++ b/src/ui/ActionLayout.tsx @@ -1,9 +1,10 @@ import clsx from 'clsx'; -import { useState, type ReactNode } from 'react'; +import { type ReactNode, useState } from 'react'; import type { ExtendedActionState } from '../api'; import { Badge } from './Badge.tsx'; import { Snackbar } from './Snackbar.tsx'; import { ExclamationShieldIcon, InfoShieldIcon, LinkIcon } from './icons'; +import ConfigIcon from './icons/ConfigIcon.tsx'; import { ActionButton, ActionDateInput, @@ -23,6 +24,7 @@ type ButtonProps = BaseButtonProps; type InputProps = BaseInputProps; export type StylePreset = 'default' | 'x-dark' | 'x-light' | 'custom'; + export enum DisclaimerType { BLOCKED = 'blocked', UNKNOWN = 'unknown', @@ -61,6 +63,7 @@ interface LayoutProps { buttons?: ButtonProps[]; inputs?: InputProps[]; form?: FormProps; + isSupported: boolean; } export interface FormProps { @@ -90,6 +93,23 @@ const Linkable = ({
{children}
); +const NotSupportedBlock = ({ className }: { className?: string }) => { + return ( +
+ +
+ +
+ This action is not supported +

Ensure you use latest blink client compatible with solana.

+ {/* TODO: replace solana with CAIP-2 chain name*/} +
+
+
+
+ ); +}; + const DisclaimerBlock = ({ type, hidden, @@ -176,6 +196,7 @@ export const ActionLayout = ({ form, error, success, + isSupported, }: LayoutProps) => { return (
@@ -248,33 +269,39 @@ export const ActionLayout = ({ {description} - {disclaimer && ( -
diff --git a/src/ui/Snackbar.tsx b/src/ui/Snackbar.tsx index 0708acd3..83a3c5f7 100644 --- a/src/ui/Snackbar.tsx +++ b/src/ui/Snackbar.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import type { ReactNode } from 'react'; -type SnackbarVariant = 'warning' | 'error'; +type SnackbarVariant = 'warning' | 'error' | 'info'; interface Props { variant?: SnackbarVariant; @@ -11,6 +11,7 @@ interface Props { const variantClasses: Record = { error: 'bg-transparent-error text-text-error border-stroke-error', warning: 'bg-transparent-warning text-text-warning border-stroke-warning', + info: 'bg-bg-secondary text-text-secondary border-none', }; export const Snackbar = ({ variant = 'warning', children }: Props) => { diff --git a/src/ui/icons/ConfigIcon.tsx b/src/ui/icons/ConfigIcon.tsx new file mode 100644 index 00000000..7df17472 --- /dev/null +++ b/src/ui/icons/ConfigIcon.tsx @@ -0,0 +1,21 @@ +import type { SVGProps } from 'react'; +const SvgComponent = (props: SVGProps) => ( + + + + +); +export default SvgComponent; From 65105c9a6ef04b0f80f9906640b961cbf49a4691 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Wed, 7 Aug 2024 00:14:43 +0300 Subject: [PATCH 07/24] cleanup --- src/api/Action/action-supportability.ts | 48 ++++++++++++------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index b8a45779..84648450 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -23,11 +23,35 @@ const BASELINE_ACTION_BLOCKCHAIN_IDS = [ BlockchainIds.SOLANA_MAINNET, // Solana mainnet CAIP-2 Blockchain ID ]; +type CheckSupportedParams = { + acceptBlockchainIds: string[]; +}; + type IsVersionCompatibleParams = { actionVersion?: string; acceptActionVersion?: string; }; +type IsBlockchainSupportedParams = { + actionBlockchainIds?: string[]; + acceptBlockchainIds: string[]; +}; + +export function defaultCheckSupported( + context: Omit, + { acceptBlockchainIds }: CheckSupportedParams, +) { + return ( + isVersionSupported({ + actionVersion: context.action.metadata.version, + }) && + isBlockchainSupported({ + acceptBlockchainIds, + actionBlockchainIds: context.action.metadata.blockchainIds, + }) + ); +} + export function isVersionSupported({ acceptActionVersion = ACCEPT_ACTION_VERSION, actionVersion = BASELINE_ACTION_VERSION, @@ -48,11 +72,6 @@ function compareSemverVersionsIgnoringPatch(v1: string, v2: string): number { return 0; } -type IsBlockchainSupportedParams = { - actionBlockchainIds?: string[]; - acceptBlockchainIds: string[]; -}; - export function isBlockchainSupported({ acceptBlockchainIds, actionBlockchainIds = BASELINE_ACTION_BLOCKCHAIN_IDS, @@ -64,22 +83,3 @@ export function isBlockchainSupported({ sanitizedAcceptBlockchainIds.includes(chain), ); } - -type CheckSupportedParams = { - acceptBlockchainIds: string[]; -}; - -export function defaultCheckSupported( - context: Omit, - { acceptBlockchainIds }: CheckSupportedParams, -) { - return ( - isBlockchainSupported({ - acceptBlockchainIds, - actionBlockchainIds: context.action.metadata.blockchainIds, - }) && - isVersionSupported({ - actionVersion: context.action.metadata.version, - }) - ); -} From ea3736ab0a88c64300064d3d57d4160cee1e20d3 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Wed, 7 Aug 2024 19:59:14 +0300 Subject: [PATCH 08/24] add action metadata in action adapter --- src/api/Action/Action.ts | 9 +- src/api/Action/action-supportability.ts | 83 +++++++++++++------ src/api/ActionConfig.ts | 57 +++++++++++-- test/api/Action/action-supportability.spec.ts | 77 ++++++++++------- 4 files changed, 157 insertions(+), 69 deletions(-) diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index 8b764f01..79f6c40c 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -118,11 +118,10 @@ export class Action { const data = (await response.json()) as ActionGetResponse; - const blockchainIds = - response.headers - .get('x-blockchain-ids') - ?.split(',') - .map((id) => id.trim()) ?? []; + const blockchainIds = response.headers + .get('x-blockchain-ids') + ?.split(',') + .map((id) => id.trim()); const version = response.headers.get('x-action-version')?.trim(); const metadata: ActionMetadata = { diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index 84648450..1fe09325 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -6,62 +6,80 @@ import type { ActionContext } from '../ActionConfig.ts'; */ export const BlockchainIds = { SOLANA_MAINNET: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + SOLANA_DEVNET: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', + SOLANA_TESTNET: 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3', }; /** * Max spec version the Blink client supports. */ -const ACCEPT_ACTION_VERSION = ACTIONS_SPEC_VERSION; +const MAX_SUPPORTED_ACTION_VERSION = ACTIONS_SPEC_VERSION; /** - * Baseline action version to be used when not set by action provider. Defaults to latest pre-versioning actions spec release. + * Baseline action version to be used when not set by action provider. + * Defaults to latest release that doesn't support versioning. */ const BASELINE_ACTION_VERSION = '1.5.1'; /** - * Baseline blockchain IDs to be used when not set by action provider. Defaults to Solana mainnet. + * Baseline blockchain IDs to be used when not set by action provider. + * Defaults to Solana mainnet. */ -const BASELINE_ACTION_BLOCKCHAIN_IDS = [ - BlockchainIds.SOLANA_MAINNET, // Solana mainnet CAIP-2 Blockchain ID -]; +const BASELINE_ACTION_BLOCKCHAIN_IDS = [BlockchainIds.SOLANA_MAINNET]; type CheckSupportedParams = { - acceptBlockchainIds: string[]; + supportedBlockchainIds: string[]; }; -type IsVersionCompatibleParams = { +type IsVersionSupportedParams = { actionVersion?: string; - acceptActionVersion?: string; + supportedActionVersion?: string; }; -type IsBlockchainSupportedParams = { +type IsBlockchainIdSupportedParams = { actionBlockchainIds?: string[]; - acceptBlockchainIds: string[]; + supportedBlockchainIds: string[]; }; +/** + * Default implementation for checking if an action is supported. + * Checks if the action version and the action blockchain IDs are supported by blink. + * @param context Action context. + * @param supportedBlockchainIds List of CAIP-2 {@link BlockchainIds} the client supports. + * + * @see {isVersionSupported} + * @see {isBlockchainSupported} + */ export function defaultCheckSupported( context: Omit, - { acceptBlockchainIds }: CheckSupportedParams, + { supportedBlockchainIds }: CheckSupportedParams, ) { + const { version: actionVersion, blockchainIds: actionBlockchainIds } = + context.action.metadata; return ( isVersionSupported({ - actionVersion: context.action.metadata.version, + actionVersion, }) && isBlockchainSupported({ - acceptBlockchainIds, - actionBlockchainIds: context.action.metadata.blockchainIds, + supportedBlockchainIds, + actionBlockchainIds, }) ); } +/** + * Check if the action version is supported by blink. + * @param supportedActionVersion The version the blink supports. + * @param actionVersion The version of the action. + * + * @returns `true` if the action version is less than or equal to the supported ignoring patch version, `false` otherwise. + */ export function isVersionSupported({ - acceptActionVersion = ACCEPT_ACTION_VERSION, + supportedActionVersion = MAX_SUPPORTED_ACTION_VERSION, actionVersion = BASELINE_ACTION_VERSION, -}: IsVersionCompatibleParams): boolean { - return ( - compareSemverVersionsIgnoringPatch(actionVersion, acceptActionVersion) <= 0 - ); +}: IsVersionSupportedParams): boolean { + return compareSemverIgnoringPatch(actionVersion, supportedActionVersion) <= 0; } -function compareSemverVersionsIgnoringPatch(v1: string, v2: string): number { +function compareSemverIgnoringPatch(v1: string, v2: string): number { const [major1, minor1] = v1.split('.').map(Number); const [major2, minor2] = v2.split('.').map(Number); if (major1 !== major2) { @@ -72,14 +90,27 @@ function compareSemverVersionsIgnoringPatch(v1: string, v2: string): number { return 0; } +/** + * Check if action blockchain IDs are supported by the blink. + * + * @param supportedBlockchainIds List of CAIP-2 blockchain IDs the client supports. + * @param actionBlockchainIds List of CAIP-2 blockchain IDs the action supports. + * + * @returns `true` if all action blockchain IDs are supported by blink, `false` otherwise. + * + * @see BlockchainIds + */ export function isBlockchainSupported({ - acceptBlockchainIds, + supportedBlockchainIds, actionBlockchainIds = BASELINE_ACTION_BLOCKCHAIN_IDS, -}: IsBlockchainSupportedParams): boolean { - const sanitizedAcceptBlockchainIds = acceptBlockchainIds.map((it) => +}: IsBlockchainIdSupportedParams): boolean { + const sanitizedSupportedBlockchainIds = supportedBlockchainIds.map((it) => + it.trim(), + ); + const sanitizedActionBlockchainIds = actionBlockchainIds.map((it) => it.trim(), ); - return actionBlockchainIds.every((chain) => - sanitizedAcceptBlockchainIds.includes(chain), + return sanitizedActionBlockchainIds.every((chain) => + sanitizedSupportedBlockchainIds.includes(chain), ); } diff --git a/src/api/ActionConfig.ts b/src/api/ActionConfig.ts index 3718072f..d7c9b01c 100644 --- a/src/api/ActionConfig.ts +++ b/src/api/ActionConfig.ts @@ -1,6 +1,10 @@ import { Connection } from '@solana/web3.js'; import { type Action } from './Action'; import { AbstractActionComponent } from './Action/action-components'; +import { + BlockchainIds, + defaultCheckSupported, +} from './Action/action-supportability.ts'; export interface ActionContext { originalUrl: string; @@ -11,10 +15,26 @@ export interface ActionContext { export interface IncomingActionConfig { rpcUrl: string; - adapter: Pick; + adapter: Pick & + Partial>; +} + +/** + * Metadata for an action adapter. + * + * @property supportedBlockchainIds List of CAIP-2 blockchain IDs the adapter supports. + * + * @see {BlockchainIds} + */ +export interface ActionAdapterMetadata { + /** + * List of CAIP-2 blockchain IDs the adapter supports. + */ + supportedBlockchainIds: string[]; } export interface ActionAdapter { + metadata: ActionAdapterMetadata; connect: (context: ActionContext) => Promise; signTransaction: ( tx: string, @@ -24,13 +44,19 @@ export interface ActionAdapter { signature: string, context: ActionContext, ) => Promise; - isSupported?: ( + isSupported: ( context: Omit, ) => Promise; } export class ActionConfig implements ActionAdapter { private static readonly CONFIRM_TIMEOUT_MS = 60000 * 1.2; // 20% extra time + private static readonly DEFAULT_METADATA: ActionAdapterMetadata = { + supportedBlockchainIds: [ + BlockchainIds.SOLANA_MAINNET, + BlockchainIds.SOLANA_DEVNET, + ], + }; private connection: Connection; constructor( @@ -47,12 +73,8 @@ export class ActionConfig implements ActionAdapter { : rpcUrlOrConnection; } - async connect(context: ActionContext) { - try { - return await this.adapter.connect(context); - } catch { - return null; - } + get metadata() { + return this.adapter.metadata ?? ActionConfig.DEFAULT_METADATA; } signTransaction(tx: string, context: ActionContext) { @@ -96,4 +118,23 @@ export class ActionConfig implements ActionAdapter { confirm(); }); } + + async connect(context: ActionContext) { + try { + return await this.adapter.connect(context); + } catch { + return null; + } + } + + async isSupported( + context: Omit, + ): Promise { + if (!this.adapter.isSupported) { + return defaultCheckSupported(context, { + supportedBlockchainIds: this.metadata.supportedBlockchainIds, + }); + } + return this.adapter.isSupported(context); + } } diff --git a/test/api/Action/action-supportability.spec.ts b/test/api/Action/action-supportability.spec.ts index 301730f8..9f02c34b 100644 --- a/test/api/Action/action-supportability.spec.ts +++ b/test/api/Action/action-supportability.spec.ts @@ -1,27 +1,24 @@ import { describe, expect, test } from 'bun:test'; -import { - isBlockchainSupported, - isVersionSupported, -} from '../../../src/api/Action/action-supportability.ts'; +import { isBlockchainSupported, isVersionSupported } from '../../../src'; describe('isVersionSupported', () => { test('returns true when action version is less than client version', () => { expect( isVersionSupported({ actionVersion: '2.1.0', - acceptActionVersion: '2.2.0', + supportedActionVersion: '2.2.0', }), ).toBe(true); expect( isVersionSupported({ actionVersion: '2.2.0', - acceptActionVersion: '2.3.0', + supportedActionVersion: '2.3.0', }), ).toBe(true); expect( isVersionSupported({ actionVersion: '1.0.0', - acceptActionVersion: '2.2.0', + supportedActionVersion: '2.2.0', }), ).toBe(true); }); @@ -30,7 +27,7 @@ describe('isVersionSupported', () => { expect( isVersionSupported({ actionVersion: '2.2.0', - acceptActionVersion: '2.2.0', + supportedActionVersion: '2.2.0', }), ).toBe(true); }); @@ -39,36 +36,50 @@ describe('isVersionSupported', () => { expect( isVersionSupported({ actionVersion: '2.3.0', - acceptActionVersion: '2.2.0', + supportedActionVersion: '2.2.0', }), ).toBe(false); expect( isVersionSupported({ actionVersion: '3.0.0', - acceptActionVersion: '2.2.0', + supportedActionVersion: '2.2.0', }), ).toBe(false); }); - test('returns true when action version is not provided and uses baseline version', () => { - expect(isVersionSupported({ acceptActionVersion: '2.2.0' })).toBe(true); + test('returns true when action version is not provided and uses baseline action version', () => { + expect(isVersionSupported({ supportedActionVersion: '1.4.1' })).toBe(false); + expect(isVersionSupported({ supportedActionVersion: '1.5.1' })).toBe(true); + expect(isVersionSupported({ supportedActionVersion: '1.6.0' })).toBe(true); }); - test('returns true when acceptActionVersion is not provided and uses default', () => { - expect(isVersionSupported({ actionVersion: '2.1.0' })).toBe(true); - expect(isVersionSupported({ actionVersion: '2.2.0' })).toBe(true); - expect(isVersionSupported({ actionVersion: '2.3.0' })).toBe(false); - }); - - test('returns true when both versions are not provided and uses baseline version', () => { + test('returns true when both versions are not provided', () => { expect(isVersionSupported({})).toBe(true); }); - test('returns true when action version has patch version less than client version', () => { + test('returns true ignoring patch version', () => { + expect( + isVersionSupported({ + actionVersion: '2.2.1', + supportedActionVersion: '2.2.2', + }), + ).toBe(true); expect( isVersionSupported({ actionVersion: '2.2.1', - acceptActionVersion: '2.2.2', + supportedActionVersion: '2.2.1', + }), + ).toBe(true); + expect( + isVersionSupported({ + actionVersion: '2.2.2', + supportedActionVersion: '2.2.1', + }), + ).toBe(true); + expect( + isVersionSupported({ + actionVersion: '2.2.2', + supportedActionVersion: '2.2', }), ).toBe(true); }); @@ -77,7 +88,13 @@ describe('isVersionSupported', () => { expect( isVersionSupported({ actionVersion: 'invalidVersion', - acceptActionVersion: '2.2.0', + supportedActionVersion: '2.2.0', + }), + ).toBe(false); + expect( + isVersionSupported({ + actionVersion: '2.2.0', + supportedActionVersion: 'invalidVersion', }), ).toBe(false); }); @@ -86,7 +103,7 @@ describe('isVersionSupported', () => { expect( isVersionSupported({ actionVersion: '2.2.0', - acceptActionVersion: 'invalid.version', + supportedActionVersion: 'invalid.version', }), ).toBe(false); }); @@ -95,7 +112,7 @@ describe('isVersionSupported', () => { expect( isVersionSupported({ actionVersion: 'invalid.version', - acceptActionVersion: 'invalid.version', + supportedActionVersion: 'invalid.version', }), ).toBe(false); }); @@ -105,7 +122,7 @@ describe('isBlockchainSupported', () => { test('returns true when all actionBlockchainIds are supported', () => { expect( isBlockchainSupported({ - acceptBlockchainIds: [ + supportedBlockchainIds: [ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', 'ethereum:1', ], @@ -117,7 +134,7 @@ describe('isBlockchainSupported', () => { test('returns false when some actionBlockchainIds are not supported', () => { expect( isBlockchainSupported({ - acceptBlockchainIds: ['ethereum:1'], + supportedBlockchainIds: ['ethereum:1'], actionBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], }), ).toBe(false); @@ -126,7 +143,7 @@ describe('isBlockchainSupported', () => { test('returns true when actionBlockchainIds is not provided and uses baseline', () => { expect( isBlockchainSupported({ - acceptBlockchainIds: [ + supportedBlockchainIds: [ 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', 'ethereum:1', ], @@ -137,7 +154,7 @@ describe('isBlockchainSupported', () => { test('returns false when actionBlockchainIds is not provided and baseline is not supported', () => { expect( isBlockchainSupported({ - acceptBlockchainIds: ['ethereum:1'], + supportedBlockchainIds: ['ethereum:1'], }), ).toBe(false); }); @@ -145,7 +162,7 @@ describe('isBlockchainSupported', () => { test('returns true when both blockchainIds and actionBlockchainIds are empty', () => { expect( isBlockchainSupported({ - acceptBlockchainIds: [], + supportedBlockchainIds: [], actionBlockchainIds: [], }), ).toBe(true); @@ -154,7 +171,7 @@ describe('isBlockchainSupported', () => { test('returns false when blockchainIds is empty and actionBlockchainIds is not', () => { expect( isBlockchainSupported({ - acceptBlockchainIds: [], + supportedBlockchainIds: [], actionBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], }), ).toBe(false); From 60392e45ff3a66a24b4264ab05463ae5f9c98e44 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Wed, 7 Aug 2024 23:37:51 +0300 Subject: [PATCH 09/24] craft action metadata aware message on action unsupported --- src/api/Action/Action.ts | 20 +++++++--- src/api/Action/action-supportability.ts | 23 +++++------ src/api/ActionConfig.ts | 6 +-- src/ext/twitter.tsx | 12 +++--- src/ui/ActionContainer.tsx | 10 ++++- src/ui/ActionLayout.tsx | 29 ++++++++++---- src/utils/caip-2.ts | 14 +++++++ src/utils/index.ts | 1 + test/api/Action/action-supportability.spec.ts | 38 +++++-------------- 9 files changed, 85 insertions(+), 68 deletions(-) create mode 100644 src/utils/caip-2.ts diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index 79f6c40c..5cfc9046 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -12,12 +12,16 @@ import { MultiValueActionComponent, SingleValueActionComponent, } from './action-components'; +import { + BASELINE_ACTION_BLOCKCHAIN_IDS, + BASELINE_ACTION_VERSION, +} from './action-supportability.ts'; const MULTI_VALUE_TYPES: ActionParameterType[] = ['checkbox']; interface ActionMetadata { - blockchainIds?: string[]; - version?: string; + blockchainIds: string[]; + version: string; } export class Action { @@ -26,7 +30,7 @@ export class Action { private constructor( private readonly _url: string, private readonly _data: ActionGetResponse, - private readonly _metadata: ActionMetadata, + private readonly _metadata: Partial, private _adapter?: ActionAdapter, ) { // if no links present, fallback to original solana pay spec @@ -76,8 +80,12 @@ export class Action { return this._data.error?.message ?? null; } - public get metadata() { - return this._metadata; + public get metadata(): ActionMetadata { + return { + blockchainIds: + this._metadata.blockchainIds ?? BASELINE_ACTION_BLOCKCHAIN_IDS, + version: this._metadata.version ?? BASELINE_ACTION_VERSION, + }; } public get adapter() { @@ -124,7 +132,7 @@ export class Action { .map((id) => id.trim()); const version = response.headers.get('x-action-version')?.trim(); - const metadata: ActionMetadata = { + const metadata: Partial = { blockchainIds, version, }; diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index 1fe09325..7617cf5c 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -1,41 +1,33 @@ +import { BlockchainIds } from '../../utils'; import { ACTIONS_SPEC_VERSION } from '../../utils/dependency-versions.ts'; import type { ActionContext } from '../ActionConfig.ts'; -/** - * CAIP-2 Blockchain IDs. - */ -export const BlockchainIds = { - SOLANA_MAINNET: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - SOLANA_DEVNET: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', - SOLANA_TESTNET: 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3', -}; - /** * Max spec version the Blink client supports. */ -const MAX_SUPPORTED_ACTION_VERSION = ACTIONS_SPEC_VERSION; +export const MAX_SUPPORTED_ACTION_VERSION = ACTIONS_SPEC_VERSION; /** * Baseline action version to be used when not set by action provider. * Defaults to latest release that doesn't support versioning. */ -const BASELINE_ACTION_VERSION = '1.5.1'; +export const BASELINE_ACTION_VERSION = '1.5.1'; /** * Baseline blockchain IDs to be used when not set by action provider. * Defaults to Solana mainnet. */ -const BASELINE_ACTION_BLOCKCHAIN_IDS = [BlockchainIds.SOLANA_MAINNET]; +export const BASELINE_ACTION_BLOCKCHAIN_IDS = [BlockchainIds.SOLANA_MAINNET]; type CheckSupportedParams = { supportedBlockchainIds: string[]; }; type IsVersionSupportedParams = { - actionVersion?: string; + actionVersion: string; supportedActionVersion?: string; }; type IsBlockchainIdSupportedParams = { - actionBlockchainIds?: string[]; + actionBlockchainIds: string[]; supportedBlockchainIds: string[]; }; @@ -104,6 +96,9 @@ export function isBlockchainSupported({ supportedBlockchainIds, actionBlockchainIds = BASELINE_ACTION_BLOCKCHAIN_IDS, }: IsBlockchainIdSupportedParams): boolean { + if (actionBlockchainIds.length === 0 || supportedBlockchainIds.length === 0) { + return false; + } const sanitizedSupportedBlockchainIds = supportedBlockchainIds.map((it) => it.trim(), ); diff --git a/src/api/ActionConfig.ts b/src/api/ActionConfig.ts index d7c9b01c..370e3507 100644 --- a/src/api/ActionConfig.ts +++ b/src/api/ActionConfig.ts @@ -1,10 +1,8 @@ import { Connection } from '@solana/web3.js'; +import { BlockchainIds } from '../utils'; import { type Action } from './Action'; import { AbstractActionComponent } from './Action/action-components'; -import { - BlockchainIds, - defaultCheckSupported, -} from './Action/action-supportability.ts'; +import { defaultCheckSupported } from './Action/action-supportability.ts'; export interface ActionContext { originalUrl: string; diff --git a/src/ext/twitter.tsx b/src/ext/twitter.tsx index a94300c2..90b46000 100644 --- a/src/ext/twitter.tsx +++ b/src/ext/twitter.tsx @@ -186,13 +186,11 @@ async function handleNewNode( return; } - const isSupported = config.isSupported - ? await config.isSupported({ - originalUrl: actionUrl.toString(), - action, - actionType: state, - }) - : true; + const isSupported = await config.isSupported({ + originalUrl: actionUrl.toString(), + action, + actionType: state, + }); addMargin(container).replaceChildren( createAction({ diff --git a/src/ui/ActionContainer.tsx b/src/ui/ActionContainer.tsx index b6c6146f..01e2e16b 100644 --- a/src/ui/ActionContainer.tsx +++ b/src/ui/ActionContainer.tsx @@ -17,6 +17,7 @@ import { SingleValueActionComponent, } from '../api'; import { checkSecurity, type SecurityLevel } from '../shared'; +import { BlockchainNames } from '../utils/caip-2.ts'; import { isInterstitial } from '../utils/interstitial-url.ts'; import { isPostRequestError, @@ -471,6 +472,10 @@ export const ActionContainer = ({ return null; }, [executionState.status, isPassingSecurityCheck, overallState]); + const actionBlockchainNames = action.metadata.blockchainIds.map( + (it) => BlockchainNames[it] ?? it, + ); + return ( asInputProps(input))} form={form ? asFormProps(form) : undefined} disclaimer={disclaimer} - isSupported={isSupported} + supportability={{ + isSupported, + actionBlockchainNames, + }} /> ); }; diff --git a/src/ui/ActionLayout.tsx b/src/ui/ActionLayout.tsx index 333b5b13..27be33c6 100644 --- a/src/ui/ActionLayout.tsx +++ b/src/ui/ActionLayout.tsx @@ -49,6 +49,11 @@ const stylePresetClassMap: Record = { custom: 'custom', }; +export interface ActionSupportability { + isSupported: boolean; + actionBlockchainNames: string[]; +} + interface LayoutProps { stylePreset?: StylePreset; image?: string; @@ -63,7 +68,7 @@ interface LayoutProps { buttons?: ButtonProps[]; inputs?: InputProps[]; form?: FormProps; - isSupported: boolean; + supportability: ActionSupportability; } export interface FormProps { @@ -93,7 +98,13 @@ const Linkable = ({
{children}
); -const NotSupportedBlock = ({ className }: { className?: string }) => { +const NotSupportedBlock = ({ + actionBlockchainIds, + className, +}: { + actionBlockchainIds: string[]; + className?: string; +}) => { return (
@@ -101,8 +112,10 @@ const NotSupportedBlock = ({ className }: { className?: string }) => {
This action is not supported -

Ensure you use latest blink client compatible with solana.

- {/* TODO: replace solana with CAIP-2 chain name*/} +

+ Make sure you are using the latest Blink client, which includes + support for {actionBlockchainIds.join(', ')}. +

@@ -196,7 +209,7 @@ export const ActionLayout = ({ form, error, success, - isSupported, + supportability, }: LayoutProps) => { return (
@@ -269,8 +282,10 @@ export const ActionLayout = ({ {description} - {!isSupported ? ( - + {!supportability.isSupported ? ( + ) : ( <> {disclaimer && ( diff --git a/src/utils/caip-2.ts b/src/utils/caip-2.ts new file mode 100644 index 00000000..99d2584d --- /dev/null +++ b/src/utils/caip-2.ts @@ -0,0 +1,14 @@ +/** + * CAIP-2 Blockchain IDs. + */ +export const BlockchainIds = { + SOLANA_MAINNET: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + SOLANA_DEVNET: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', + SOLANA_TESTNET: 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3', +}; + +export const BlockchainNames: Record = { + [BlockchainIds.SOLANA_MAINNET]: 'solana:mainnet', + [BlockchainIds.SOLANA_DEVNET]: 'solana:devnet', + [BlockchainIds.SOLANA_TESTNET]: 'solana:testnet', +}; diff --git a/src/utils/index.ts b/src/utils/index.ts index 5493d870..50170126 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1 +1,2 @@ +export { BlockchainIds } from './caip-2.ts'; export { setProxyUrl } from './proxify'; diff --git a/test/api/Action/action-supportability.spec.ts b/test/api/Action/action-supportability.spec.ts index 9f02c34b..b036c468 100644 --- a/test/api/Action/action-supportability.spec.ts +++ b/test/api/Action/action-supportability.spec.ts @@ -47,16 +47,6 @@ describe('isVersionSupported', () => { ).toBe(false); }); - test('returns true when action version is not provided and uses baseline action version', () => { - expect(isVersionSupported({ supportedActionVersion: '1.4.1' })).toBe(false); - expect(isVersionSupported({ supportedActionVersion: '1.5.1' })).toBe(true); - expect(isVersionSupported({ supportedActionVersion: '1.6.0' })).toBe(true); - }); - - test('returns true when both versions are not provided', () => { - expect(isVersionSupported({})).toBe(true); - }); - test('returns true ignoring patch version', () => { expect( isVersionSupported({ @@ -140,39 +130,29 @@ describe('isBlockchainSupported', () => { ).toBe(false); }); - test('returns true when actionBlockchainIds is not provided and uses baseline', () => { - expect( - isBlockchainSupported({ - supportedBlockchainIds: [ - 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', - 'ethereum:1', - ], - }), - ).toBe(true); - }); - - test('returns false when actionBlockchainIds is not provided and baseline is not supported', () => { + test('returns false when both blockchainIds and actionBlockchainIds are empty', () => { expect( isBlockchainSupported({ - supportedBlockchainIds: ['ethereum:1'], + supportedBlockchainIds: [], + actionBlockchainIds: [], }), ).toBe(false); }); - test('returns true when both blockchainIds and actionBlockchainIds are empty', () => { + test('returns false when supportedBlockchainIds is empty and actionBlockchainIds is not', () => { expect( isBlockchainSupported({ supportedBlockchainIds: [], - actionBlockchainIds: [], + actionBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], }), - ).toBe(true); + ).toBe(false); }); - test('returns false when blockchainIds is empty and actionBlockchainIds is not', () => { + test('returns false when actionBlockchainIds is empty and actionBlockchainIds is not', () => { expect( isBlockchainSupported({ - supportedBlockchainIds: [], - actionBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + supportedBlockchainIds: ['solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'], + actionBlockchainIds: [], }), ).toBe(false); }); From 5059196c77cf1acbd45c90dabe1672ff8fdf37be Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Thu, 8 Aug 2024 15:15:28 +0300 Subject: [PATCH 10/24] set baseline version to 2.0.0 --- src/api/Action/action-supportability.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index 7617cf5c..2fcf6b97 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -10,7 +10,7 @@ export const MAX_SUPPORTED_ACTION_VERSION = ACTIONS_SPEC_VERSION; * Baseline action version to be used when not set by action provider. * Defaults to latest release that doesn't support versioning. */ -export const BASELINE_ACTION_VERSION = '1.5.1'; +export const BASELINE_ACTION_VERSION = '2.0.0'; /** * Baseline blockchain IDs to be used when not set by action provider. * Defaults to Solana mainnet. @@ -66,7 +66,7 @@ export function defaultCheckSupported( */ export function isVersionSupported({ supportedActionVersion = MAX_SUPPORTED_ACTION_VERSION, - actionVersion = BASELINE_ACTION_VERSION, + actionVersion, }: IsVersionSupportedParams): boolean { return compareSemverIgnoringPatch(actionVersion, supportedActionVersion) <= 0; } @@ -94,7 +94,7 @@ function compareSemverIgnoringPatch(v1: string, v2: string): number { */ export function isBlockchainSupported({ supportedBlockchainIds, - actionBlockchainIds = BASELINE_ACTION_BLOCKCHAIN_IDS, + actionBlockchainIds, }: IsBlockchainIdSupportedParams): boolean { if (actionBlockchainIds.length === 0 || supportedBlockchainIds.length === 0) { return false; From a8ba440bf361622c7b9a875c6b5f909213e745d0 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Thu, 8 Aug 2024 16:04:31 +0300 Subject: [PATCH 11/24] add todo to remove defaults --- src/api/Action/Action.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index 5cfc9046..4b408ca9 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -82,6 +82,7 @@ export class Action { public get metadata(): ActionMetadata { return { + // TODO: remove defaults after a few weeks and make action incompatible if metadata not set by action provider blockchainIds: this._metadata.blockchainIds ?? BASELINE_ACTION_BLOCKCHAIN_IDS, version: this._metadata.version ?? BASELINE_ACTION_VERSION, From 997fe32ebe273025b739798d0e408b68a20dea52 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Fri, 9 Aug 2024 17:15:38 +0300 Subject: [PATCH 12/24] WIP move compatibility strategy outside of ActionAdapter --- src/api/Action/Action.ts | 32 +++++-- src/api/Action/action-supportability.ts | 93 ++++++++++++++----- src/api/ActionConfig.ts | 17 +--- src/ext/twitter.tsx | 29 +++--- .../solana/useActionSolanaWalletAdapter.ts | 4 +- src/hooks/useAction.ts | 16 +++- src/ui/ActionContainer.tsx | 26 +++--- src/ui/ActionLayout.tsx | 20 +--- src/ui/Checkbox.tsx | 4 +- 9 files changed, 146 insertions(+), 95 deletions(-) diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index 4b408ca9..7d31ec94 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -13,6 +13,7 @@ import { SingleValueActionComponent, } from './action-components'; import { + type ActionSupportStrategy, BASELINE_ACTION_BLOCKCHAIN_IDS, BASELINE_ACTION_VERSION, } from './action-supportability.ts'; @@ -20,8 +21,8 @@ import { const MULTI_VALUE_TYPES: ActionParameterType[] = ['checkbox']; interface ActionMetadata { - blockchainIds: string[]; - version: string; + blockchainIds?: string[]; + version?: string; } export class Action { @@ -30,7 +31,8 @@ export class Action { private constructor( private readonly _url: string, private readonly _data: ActionGetResponse, - private readonly _metadata: Partial, + private readonly _metadata: ActionMetadata, + private readonly _supportStrategy: ActionSupportStrategy, private _adapter?: ActionAdapter, ) { // if no links present, fallback to original solana pay spec @@ -80,15 +82,19 @@ export class Action { return this._data.error?.message ?? null; } - public get metadata(): ActionMetadata { + public get metadata(): Required { + // TODO: Change fallback to baseline version after a few weeks after proxies adopt versioning and remove Required return { - // TODO: remove defaults after a few weeks and make action incompatible if metadata not set by action provider blockchainIds: this._metadata.blockchainIds ?? BASELINE_ACTION_BLOCKCHAIN_IDS, version: this._metadata.version ?? BASELINE_ACTION_VERSION, }; } + public get adapterUnsafe() { + return this._adapter; + } + public get adapter() { if (!this._adapter) { throw new Error('No adapter provided'); @@ -101,17 +107,25 @@ export class Action { this._adapter = adapter; } + public isSupported() { + return this._supportStrategy(this); + } + // be sure to use this only if the action is valid static hydrate( url: string, data: ActionGetResponse, metadata: ActionMetadata, + supportStrategy: ActionSupportStrategy, adapter?: ActionAdapter, ) { - return new Action(url, data, metadata, adapter); + return new Action(url, data, metadata, supportStrategy, adapter); } - static async fetch(apiUrl: string, adapter?: ActionAdapter) { + static async fromApiUrl( + apiUrl: string, + supportStrategy: ActionSupportStrategy, + ) { const proxyUrl = proxify(apiUrl); const response = await fetch(proxyUrl, { headers: { @@ -133,12 +147,12 @@ export class Action { .map((id) => id.trim()); const version = response.headers.get('x-action-version')?.trim(); - const metadata: Partial = { + const metadata: ActionMetadata = { blockchainIds, version, }; - return new Action(apiUrl, data, metadata, adapter); + return new Action(apiUrl, data, metadata, supportStrategy); } } diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index 2fcf6b97..61fa2463 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -1,11 +1,18 @@ import { BlockchainIds } from '../../utils'; +import { BlockchainNames } from '../../utils/caip-2.ts'; import { ACTIONS_SPEC_VERSION } from '../../utils/dependency-versions.ts'; -import type { ActionContext } from '../ActionConfig.ts'; +import type { Action } from './Action.ts'; /** * Max spec version the Blink client supports. */ export const MAX_SUPPORTED_ACTION_VERSION = ACTIONS_SPEC_VERSION; + +export const DEFAULT_SUPPORTED_BLOCKCHAIN_IDS = [ + BlockchainIds.SOLANA_MAINNET, + BlockchainIds.SOLANA_DEVNET, +]; + /** * Baseline action version to be used when not set by action provider. * Defaults to latest release that doesn't support versioning. @@ -17,13 +24,9 @@ export const BASELINE_ACTION_VERSION = '2.0.0'; */ export const BASELINE_ACTION_BLOCKCHAIN_IDS = [BlockchainIds.SOLANA_MAINNET]; -type CheckSupportedParams = { - supportedBlockchainIds: string[]; -}; - type IsVersionSupportedParams = { actionVersion: string; - supportedActionVersion?: string; + supportedActionVersion: string; }; type IsBlockchainIdSupportedParams = { @@ -31,31 +34,75 @@ type IsBlockchainIdSupportedParams = { supportedBlockchainIds: string[]; }; +export type ActionSupportability = + | { + isSupported: true; + } + | { + isSupported: false; + message: string; + }; + +export type ActionSupportStrategy = ( + action: Action, +) => Promise; + /** * Default implementation for checking if an action is supported. * Checks if the action version and the action blockchain IDs are supported by blink. - * @param context Action context. - * @param supportedBlockchainIds List of CAIP-2 {@link BlockchainIds} the client supports. + * @param action Action. * * @see {isVersionSupported} * @see {isBlockchainSupported} */ -export function defaultCheckSupported( - context: Omit, - { supportedBlockchainIds }: CheckSupportedParams, -) { +export const defaultActionSupportStrategy: ActionSupportStrategy = async ( + action, +) => { const { version: actionVersion, blockchainIds: actionBlockchainIds } = - context.action.metadata; - return ( - isVersionSupported({ - actionVersion, - }) && - isBlockchainSupported({ - supportedBlockchainIds, - actionBlockchainIds, - }) + action.metadata; + const supportedActionVersion = MAX_SUPPORTED_ACTION_VERSION; + const supportedBlockchainIds = !action.adapterUnsafe + ? action.metadata.blockchainIds // Assuming action is supported if this happens for optimistic compatibility + : action.adapterUnsafe.metadata.supportedBlockchainIds; + + const versionSupported = isVersionSupported({ + actionVersion, + supportedActionVersion, + }); + const blockchainSupported = isBlockchainSupported({ + actionBlockchainIds, + supportedBlockchainIds, + }); + + const actionBlockchainNames = actionBlockchainIds.map( + (id) => BlockchainNames[id] ?? id, ); -} + + if (!versionSupported && !blockchainSupported) { + return { + isSupported: false, + message: `Ensure you are using the Blink client >= ${actionVersion}, which includes + support for ${actionBlockchainNames.join(', ')}.`, + }; + } + + if (!versionSupported) { + return { + isSupported: false, + message: `Ensure you are using the Blink client >= ${actionVersion}.`, + }; + } + + if (!blockchainSupported) { + return { + isSupported: false, + message: `Ensure you are using the Blink client >= ${actionVersion}.`, + }; + } + return { + isSupported: true, + }; +}; /** * Check if the action version is supported by blink. @@ -65,7 +112,7 @@ export function defaultCheckSupported( * @returns `true` if the action version is less than or equal to the supported ignoring patch version, `false` otherwise. */ export function isVersionSupported({ - supportedActionVersion = MAX_SUPPORTED_ACTION_VERSION, + supportedActionVersion, actionVersion, }: IsVersionSupportedParams): boolean { return compareSemverIgnoringPatch(actionVersion, supportedActionVersion) <= 0; diff --git a/src/api/ActionConfig.ts b/src/api/ActionConfig.ts index 370e3507..7367f354 100644 --- a/src/api/ActionConfig.ts +++ b/src/api/ActionConfig.ts @@ -2,7 +2,6 @@ import { Connection } from '@solana/web3.js'; import { BlockchainIds } from '../utils'; import { type Action } from './Action'; import { AbstractActionComponent } from './Action/action-components'; -import { defaultCheckSupported } from './Action/action-supportability.ts'; export interface ActionContext { originalUrl: string; @@ -14,7 +13,7 @@ export interface ActionContext { export interface IncomingActionConfig { rpcUrl: string; adapter: Pick & - Partial>; + Partial>; } /** @@ -42,9 +41,6 @@ export interface ActionAdapter { signature: string, context: ActionContext, ) => Promise; - isSupported: ( - context: Omit, - ) => Promise; } export class ActionConfig implements ActionAdapter { @@ -124,15 +120,4 @@ export class ActionConfig implements ActionAdapter { return null; } } - - async isSupported( - context: Omit, - ): Promise { - if (!this.adapter.isSupported) { - return defaultCheckSupported(context, { - supportedBlockchainIds: this.metadata.supportedBlockchainIds, - }); - } - return this.adapter.isSupported(context); - } } diff --git a/src/ext/twitter.tsx b/src/ext/twitter.tsx index 90b46000..d887c823 100644 --- a/src/ext/twitter.tsx +++ b/src/ext/twitter.tsx @@ -1,19 +1,21 @@ import { createRoot } from 'react-dom/client'; import { Action, + type ActionAdapter, + type ActionCallbacksConfig, ActionsRegistry, + type ActionSupportStrategy, + defaultActionSupportStrategy, getExtendedActionState, getExtendedInterstitialState, getExtendedWebsiteState, - type ActionAdapter, - type ActionCallbacksConfig, } from '../api'; import { checkSecurity, type SecurityLevel } from '../shared'; import { ActionContainer, type StylePreset } from '../ui'; import { noop } from '../utils/constants'; import { isInterstitial } from '../utils/interstitial-url.ts'; import { proxify } from '../utils/proxify.ts'; -import { ActionsURLMapper, type ActionsJsonConfig } from '../utils/url-mapper'; +import { type ActionsJsonConfig, ActionsURLMapper } from '../utils/url-mapper'; type ObserverSecurityLevel = SecurityLevel; @@ -22,6 +24,7 @@ export interface ObserverOptions { securityLevel: | ObserverSecurityLevel | Record<'websites' | 'interstitials' | 'actions', ObserverSecurityLevel>; + supportStrategy: ActionSupportStrategy; } interface NormalizedObserverOptions { @@ -29,10 +32,12 @@ interface NormalizedObserverOptions { 'websites' | 'interstitials' | 'actions', ObserverSecurityLevel >; + supportStrategy: ActionSupportStrategy; } const DEFAULT_OPTIONS: ObserverOptions = { securityLevel: 'only-trusted', + supportStrategy: defaultActionSupportStrategy, }; const normalizeOptions = ( @@ -100,6 +105,7 @@ export function setupTwitterObserver( observer.observe(twitterReactRoot, { childList: true, subtree: true }); }); } + async function handleNewNode( node: Element, config: ActionAdapter, @@ -180,18 +186,15 @@ async function handleNewNode( return; } - const action = await Action.fetch(actionApiUrl, config).catch(noop); + const action = await Action.fromApiUrl( + actionApiUrl, + options.supportStrategy, + ).catch(noop); if (!action) { return; } - const isSupported = await config.isSupported({ - originalUrl: actionUrl.toString(), - action, - actionType: state, - }); - addMargin(container).replaceChildren( createAction({ originalUrl: actionUrl, @@ -199,7 +202,6 @@ async function handleNewNode( callbacks, options, isInterstitial: interstitialData.isInterstitial, - isSupported, }), ); } @@ -209,14 +211,12 @@ function createAction({ action, callbacks, options, - isSupported, }: { originalUrl: URL; action: Action; callbacks: Partial; options: NormalizedObserverOptions; isInterstitial: boolean; - isSupported: boolean; }) { const container = document.createElement('div'); container.className = 'dialect-action-root-container'; @@ -232,7 +232,7 @@ function createAction({ websiteText={originalUrl.hostname} callbacks={callbacks} securityLevel={options.securityLevel} - isSupported={isSupported} + supportStrategy={options.supportStrategy} />
, ); @@ -290,6 +290,7 @@ function findLinkPreview(element: Element) { return anchor ? { anchor, card } : null; } + function findLastLinkInText(element: Element) { const tweetText = findElementByTestId(element, 'tweetText'); if (!tweetText) { diff --git a/src/hooks/solana/useActionSolanaWalletAdapter.ts b/src/hooks/solana/useActionSolanaWalletAdapter.ts index ff01818d..ca802393 100644 --- a/src/hooks/solana/useActionSolanaWalletAdapter.ts +++ b/src/hooks/solana/useActionSolanaWalletAdapter.ts @@ -13,7 +13,9 @@ import { ActionConfig } from '../../api'; * @param rpcUrlOrConnection * @see {Action} */ -export function useActionSolanaWalletAdapter(rpcUrlOrConnection: string | Connection) { +export function useActionSolanaWalletAdapter( + rpcUrlOrConnection: string | Connection, +) { const wallet = useWallet(); const walletModal = useWalletModal(); diff --git a/src/hooks/useAction.ts b/src/hooks/useAction.ts index a21fcd09..e79ca22f 100644 --- a/src/hooks/useAction.ts +++ b/src/hooks/useAction.ts @@ -1,6 +1,11 @@ 'use client'; import { useEffect, useState } from 'react'; -import { Action, type ActionAdapter } from '../api'; +import { + Action, + type ActionAdapter, + type ActionSupportStrategy, + defaultActionSupportStrategy, +} from '../api'; import { unfurlUrlToActionApiUrl } from '../utils/url-mapper.ts'; import { useActionsRegistryInterval } from './useActionRegistryInterval.ts'; @@ -8,6 +13,7 @@ interface UseActionOptions { url: string | URL; adapter: ActionAdapter; securityRegistryRefreshInterval?: number; + supportStrategy?: ActionSupportStrategy; } function useActionApiUrl(url: string | URL) { @@ -36,7 +42,11 @@ function useActionApiUrl(url: string | URL) { return { actionApiUrl: apiUrl }; } -export function useAction({ url, adapter }: UseActionOptions) { +export function useAction({ + url, + adapter, + supportStrategy = defaultActionSupportStrategy, +}: UseActionOptions) { const { isRegistryLoaded } = useActionsRegistryInterval(); const { actionApiUrl } = useActionApiUrl(url); const [action, setAction] = useState(null); @@ -49,7 +59,7 @@ export function useAction({ url, adapter }: UseActionOptions) { } let ignore = false; - Action.fetch(actionApiUrl) + Action.fromApiUrl(actionApiUrl, supportStrategy) .then((action) => { if (ignore) { return; diff --git a/src/ui/ActionContainer.tsx b/src/ui/ActionContainer.tsx index 01e2e16b..8779b5ba 100644 --- a/src/ui/ActionContainer.tsx +++ b/src/ui/ActionContainer.tsx @@ -4,7 +4,9 @@ import { Action, type ActionCallbacksConfig, type ActionContext, + type ActionSupportStrategy, ButtonActionComponent, + defaultActionSupportStrategy, type ExtendedActionState, FormActionComponent, getExtendedActionState, @@ -17,7 +19,6 @@ import { SingleValueActionComponent, } from '../api'; import { checkSecurity, type SecurityLevel } from '../shared'; -import { BlockchainNames } from '../utils/caip-2.ts'; import { isInterstitial } from '../utils/interstitial-url.ts'; import { isPostRequestError, @@ -205,8 +206,8 @@ export const ActionContainer = ({ callbacks, securityLevel = DEFAULT_SECURITY_LEVEL, stylePreset = 'default', + supportStrategy = defaultActionSupportStrategy, Experimental__ActionLayout = ActionLayout, - isSupported, }: { action: Action; websiteUrl?: string | null; @@ -214,8 +215,7 @@ export const ActionContainer = ({ callbacks?: Partial; securityLevel?: SecurityLevel | NormalizedSecurityLevel; stylePreset?: StylePreset; - isSupported: boolean; - + supportStrategy?: ActionSupportStrategy; // please do not use it yet, better api is coming.. Experimental__ActionLayout?: typeof ActionLayout; }) => { @@ -267,6 +267,11 @@ export const ActionContainer = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [callbacks, action, websiteUrl]); + // const supportability: ActionSupportability = useMemo(() => { + // const actionSupportability = action.isSupported(); + // return actionSupportability; + // }, [action]); + const buttons = useMemo( () => action?.actions @@ -472,10 +477,6 @@ export const ActionContainer = ({ return null; }, [executionState.status, isPassingSecurityCheck, overallState]); - const actionBlockchainNames = action.metadata.blockchainIds.map( - (it) => BlockchainNames[it] ?? it, - ); - return ( asInputProps(input))} form={form ? asFormProps(form) : undefined} disclaimer={disclaimer} - supportability={{ - isSupported, - actionBlockchainNames, - }} + supportability={supportability} + // supportability={{ + // isSupported, + // actionBlockchainNames, + // }} /> ); }; diff --git a/src/ui/ActionLayout.tsx b/src/ui/ActionLayout.tsx index 27be33c6..bd32edfd 100644 --- a/src/ui/ActionLayout.tsx +++ b/src/ui/ActionLayout.tsx @@ -1,6 +1,6 @@ import clsx from 'clsx'; import { type ReactNode, useState } from 'react'; -import type { ExtendedActionState } from '../api'; +import type { ActionSupportability, ExtendedActionState } from '../api'; import { Badge } from './Badge.tsx'; import { Snackbar } from './Snackbar.tsx'; import { ExclamationShieldIcon, InfoShieldIcon, LinkIcon } from './icons'; @@ -49,11 +49,6 @@ const stylePresetClassMap: Record = { custom: 'custom', }; -export interface ActionSupportability { - isSupported: boolean; - actionBlockchainNames: string[]; -} - interface LayoutProps { stylePreset?: StylePreset; image?: string; @@ -99,10 +94,10 @@ const Linkable = ({ ); const NotSupportedBlock = ({ - actionBlockchainIds, + message, className, }: { - actionBlockchainIds: string[]; + message: string; className?: string; }) => { return ( @@ -112,10 +107,7 @@ const NotSupportedBlock = ({
This action is not supported -

- Make sure you are using the latest Blink client, which includes - support for {actionBlockchainIds.join(', ')}. -

+

{message}.

@@ -283,9 +275,7 @@ export const ActionLayout = ({ {description} {!supportability.isSupported ? ( - + ) : ( <> {disclaimer && ( diff --git a/src/ui/Checkbox.tsx b/src/ui/Checkbox.tsx index c2b49977..4a42dfc2 100644 --- a/src/ui/Checkbox.tsx +++ b/src/ui/Checkbox.tsx @@ -45,10 +45,10 @@ export const Checkbox = ({ 'mt-0.5 flex aspect-square h-[16px] items-center justify-center rounded-lg border transition-colors motion-reduce:transition-none', { 'border-input-stroke bg-input-bg': !value && !disabled, - 'bg-input-bg-selected border-input-stroke-selected': + 'border-input-stroke-selected bg-input-bg-selected': value && !disabled, 'border-input-stroke-disabled bg-input-bg': !value && disabled, - 'bg-input-bg-disabled border-input-stroke-disabled': + 'border-input-stroke-disabled bg-input-bg-disabled': value && disabled, }, )} From c30b3d4bc39c61a34ff5d89b270391bc82a06e84 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Fri, 9 Aug 2024 17:38:44 +0300 Subject: [PATCH 13/24] implement async compatibility check inside action container --- src/ext/twitter.tsx | 1 - src/ui/ActionContainer.tsx | 56 ++++++++++++++++++++++++++++++-------- 2 files changed, 45 insertions(+), 12 deletions(-) diff --git a/src/ext/twitter.tsx b/src/ext/twitter.tsx index d887c823..62d601be 100644 --- a/src/ext/twitter.tsx +++ b/src/ext/twitter.tsx @@ -232,7 +232,6 @@ function createAction({ websiteText={originalUrl.hostname} callbacks={callbacks} securityLevel={options.securityLevel} - supportStrategy={options.supportStrategy} /> , ); diff --git a/src/ui/ActionContainer.tsx b/src/ui/ActionContainer.tsx index 8779b5ba..bffaa5c7 100644 --- a/src/ui/ActionContainer.tsx +++ b/src/ui/ActionContainer.tsx @@ -4,9 +4,8 @@ import { Action, type ActionCallbacksConfig, type ActionContext, - type ActionSupportStrategy, + type ActionSupportability, ButtonActionComponent, - defaultActionSupportStrategy, type ExtendedActionState, FormActionComponent, getExtendedActionState, @@ -31,16 +30,24 @@ import { type StylePreset, } from './ActionLayout'; -type ExecutionStatus = 'blocked' | 'idle' | 'executing' | 'success' | 'error'; +type ExecutionStatus = + | 'blocked' + | 'checking-supportability' + | 'idle' + | 'executing' + | 'success' + | 'error'; interface ExecutionState { status: ExecutionStatus; + checkingSupportability?: boolean; executingAction?: AbstractActionComponent | null; errorMessage?: string | null; successMessage?: string | null; } enum ExecutionType { + CHECK_SUPPORTABILITY = 'CHECK_SUPPORTABILITY', INITIATE = 'INITIATE', FINISH = 'FINISH', FAIL = 'FAIL', @@ -51,6 +58,9 @@ enum ExecutionType { } type ActionValue = + | { + type: ExecutionType.CHECK_SUPPORTABILITY; + } | { type: ExecutionType.INITIATE; executingAction: AbstractActionComponent; @@ -83,6 +93,11 @@ const executionReducer = ( action: ActionValue, ): ExecutionState => { switch (action.type) { + case ExecutionType.CHECK_SUPPORTABILITY: + return { + status: 'checking-supportability', + checkingSupportability: true, + }; case ExecutionType.INITIATE: return { status: 'executing', executingAction: action.executingAction }; case ExecutionType.FINISH: @@ -126,6 +141,7 @@ const buttonVariantMap: Record< ExecutionStatus, 'default' | 'error' | 'success' > = { + 'checking-supportability': 'default', blocked: 'default', idle: 'default', executing: 'default', @@ -134,6 +150,7 @@ const buttonVariantMap: Record< }; const buttonLabelMap: Record = { + 'checking-supportability': 'Loading', blocked: null, idle: null, executing: 'Executing', @@ -206,7 +223,6 @@ export const ActionContainer = ({ callbacks, securityLevel = DEFAULT_SECURITY_LEVEL, stylePreset = 'default', - supportStrategy = defaultActionSupportStrategy, Experimental__ActionLayout = ActionLayout, }: { action: Action; @@ -215,7 +231,6 @@ export const ActionContainer = ({ callbacks?: Partial; securityLevel?: SecurityLevel | NormalizedSecurityLevel; stylePreset?: StylePreset; - supportStrategy?: ActionSupportStrategy; // please do not use it yet, better api is coming.. Experimental__ActionLayout?: typeof ActionLayout; }) => { @@ -234,6 +249,11 @@ export const ActionContainer = ({ const [actionState, setActionState] = useState( getOverallActionState(action, websiteUrl), ); + + const [supportability, setSupportability] = useState({ + isSupported: true, + }); + const overallState = useMemo( () => mergeActionStates( @@ -267,6 +287,20 @@ export const ActionContainer = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [callbacks, action, websiteUrl]); + useEffect(() => { + const checkSupportability = async (action: Action) => { + try { + dispatch({ type: ExecutionType.CHECK_SUPPORTABILITY }); + const supportability = await action.isSupported(); + setSupportability(supportability); + } finally { + dispatch({ type: ExecutionType.RESET }); + } + }; + + checkSupportability(action); + }, [action]); + // const supportability: ActionSupportability = useMemo(() => { // const actionSupportability = action.isSupported(); // return actionSupportability; @@ -405,10 +439,12 @@ export const ActionContainer = ({ } }; + // TODO: disable stuff based on supportability state const asButtonProps = (it: ButtonActionComponent) => ({ text: buttonLabelMap[executionState.status] ?? it.label, loading: - executionState.status === 'executing' && + (executionState.status === 'executing' || + executionState.status === 'checking-supportability') && it === executionState.executingAction, disabled: action.disabled || executionState.status !== 'idle', variant: buttonVariantMap[executionState.status], @@ -462,7 +498,9 @@ export const ActionContainer = ({ return { type: DisclaimerType.BLOCKED, ignorable: isPassingSecurityCheck, - hidden: executionState.status !== 'blocked', + hidden: + executionState.status !== 'blocked' && + executionState.status !== 'checking-supportability', onSkip: () => dispatch({ type: ExecutionType.UNBLOCK }), }; } @@ -497,10 +535,6 @@ export const ActionContainer = ({ form={form ? asFormProps(form) : undefined} disclaimer={disclaimer} supportability={supportability} - // supportability={{ - // isSupported, - // actionBlockchainNames, - // }} /> ); }; From 2b4491cf5ef44dd90930219ccbbdd6abd4cae351 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Fri, 9 Aug 2024 17:51:54 +0300 Subject: [PATCH 14/24] better human readable messages on errors --- src/api/Action/action-supportability.ts | 16 ++++++++++++---- src/utils/caip-2.ts | 8 +++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index 61fa2463..a7a1b041 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -79,24 +79,32 @@ export const defaultActionSupportStrategy: ActionSupportStrategy = async ( ); if (!versionSupported && !blockchainSupported) { + const blockchainMessage = + actionBlockchainIds.length === 1 + ? `blockchain ${actionBlockchainNames[0]}` + : `blockchains ${actionBlockchainNames.join(', ')}`; return { isSupported: false, - message: `Ensure you are using the Blink client >= ${actionVersion}, which includes - support for ${actionBlockchainNames.join(', ')}.`, + message: `Action version ${actionVersion} and ${blockchainMessage} are not supported by the Blink client.`, }; } if (!versionSupported) { return { isSupported: false, - message: `Ensure you are using the Blink client >= ${actionVersion}.`, + message: `Action version is not supported by the Blink client.`, }; } if (!blockchainSupported) { + const blockchainMessage = + actionBlockchainIds.length === 1 + ? `Action blockchain ${actionBlockchainNames[0]} is not supported by the Blink client.` + : `Action blockchains ${actionBlockchainNames.join(', ')} are not supported by the Blink client.`; + return { isSupported: false, - message: `Ensure you are using the Blink client >= ${actionVersion}.`, + message: blockchainMessage, }; } return { diff --git a/src/utils/caip-2.ts b/src/utils/caip-2.ts index 99d2584d..ba56f9f3 100644 --- a/src/utils/caip-2.ts +++ b/src/utils/caip-2.ts @@ -8,7 +8,9 @@ export const BlockchainIds = { }; export const BlockchainNames: Record = { - [BlockchainIds.SOLANA_MAINNET]: 'solana:mainnet', - [BlockchainIds.SOLANA_DEVNET]: 'solana:devnet', - [BlockchainIds.SOLANA_TESTNET]: 'solana:testnet', + [BlockchainIds.SOLANA_MAINNET]: 'Solana Mainnet', + [BlockchainIds.SOLANA_DEVNET]: 'Solana Devnet', + [BlockchainIds.SOLANA_TESTNET]: 'Solana Testnet', }; + +// TODO: add ethereum, and a few more blockchains From eda8dd0c2db58d4249131bb147592416fddad70a Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Sat, 10 Aug 2024 11:12:40 +0300 Subject: [PATCH 15/24] add ethereum mainnet to caip-2 constants --- src/utils/caip-2.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/caip-2.ts b/src/utils/caip-2.ts index ba56f9f3..1f739c56 100644 --- a/src/utils/caip-2.ts +++ b/src/utils/caip-2.ts @@ -5,12 +5,12 @@ export const BlockchainIds = { SOLANA_MAINNET: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', SOLANA_DEVNET: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', SOLANA_TESTNET: 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3', + ETHEREUM_MAINNET: 'eip155:1', }; export const BlockchainNames: Record = { [BlockchainIds.SOLANA_MAINNET]: 'Solana Mainnet', [BlockchainIds.SOLANA_DEVNET]: 'Solana Devnet', [BlockchainIds.SOLANA_TESTNET]: 'Solana Testnet', + [BlockchainIds.ETHEREUM_MAINNET]: 'Ethereum Mainnet', }; - -// TODO: add ethereum, and a few more blockchains From 907d88a5c7838a6f5416cdc097da63fddd5f623d Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 12:45:26 +0300 Subject: [PATCH 16/24] merge with main --- src/api/Action/Action.ts | 46 ++++++++++++++++++------- src/api/Action/action-supportability.ts | 3 +- src/ext/twitter.tsx | 3 +- src/hooks/useAction.ts | 2 +- src/ui/ActionContainer.tsx | 6 ++-- 5 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index 59b1ca4a..fb151273 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -21,6 +21,7 @@ import { type ActionSupportStrategy, BASELINE_ACTION_BLOCKCHAIN_IDS, BASELINE_ACTION_VERSION, + defaultActionSupportStrategy, } from './action-supportability.ts'; const MULTI_VALUE_TYPES: ActionParameterType[] = ['checkbox']; @@ -143,10 +144,17 @@ export class Action { chainData?: N extends PostNextActionLink ? NextActionPostRequest : never, ): Promise { if (next.type === 'inline') { - return new Action(this.url, next.action, this.metadata, this.adapter, { - isChained: true, - isInline: true, - }); + return new Action( + this.url, + next.action, + this.metadata, + this._supportStrategy, + this.adapter, + { + isChained: true, + isInline: true, + }, + ); } const baseUrlObj = new URL(this.url); @@ -181,10 +189,17 @@ export class Action { const data = (await response.json()) as NextAction; const metadata = getActionMetadata(response); - return new Action(href, data, metadata, this.adapter, { - isChained: true, - isInline: false, - }); + return new Action( + href, + data, + metadata, + this._supportStrategy, + this.adapter, + { + isChained: true, + isInline: false, + }, + ); } // be sure to use this only if the action is valid @@ -195,12 +210,13 @@ export class Action { supportStrategy: ActionSupportStrategy, adapter?: ActionAdapter, ) { - return new Action(url, data, metadata, adapter); + return new Action(url, data, metadata, supportStrategy, adapter); } - static async fromApiUrl( + static async fetch( apiUrl: string, - supportStrategy: ActionSupportStrategy, + adapter?: ActionAdapter, + supportStrategy: ActionSupportStrategy = defaultActionSupportStrategy, ) { const proxyUrl = proxify(apiUrl); const response = await fetch(proxyUrl, { @@ -218,7 +234,13 @@ export class Action { const data = (await response.json()) as ActionGetResponse; const metadata = getActionMetadata(response); - return new Action(apiUrl, { ...data, type: 'action' }, metadata, adapter); + return new Action( + apiUrl, + { ...data, type: 'action' }, + metadata, + supportStrategy, + adapter, + ); } } diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index a7a1b041..ddac0283 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -1,5 +1,4 @@ -import { BlockchainIds } from '../../utils'; -import { BlockchainNames } from '../../utils/caip-2.ts'; +import { BlockchainIds, BlockchainNames } from '../../utils/caip-2.ts'; import { ACTIONS_SPEC_VERSION } from '../../utils/dependency-versions.ts'; import type { Action } from './Action.ts'; diff --git a/src/ext/twitter.tsx b/src/ext/twitter.tsx index 62d601be..8329e4e9 100644 --- a/src/ext/twitter.tsx +++ b/src/ext/twitter.tsx @@ -186,8 +186,9 @@ async function handleNewNode( return; } - const action = await Action.fromApiUrl( + const action = await Action.fetch( actionApiUrl, + config, options.supportStrategy, ).catch(noop); diff --git a/src/hooks/useAction.ts b/src/hooks/useAction.ts index e79ca22f..8715d30d 100644 --- a/src/hooks/useAction.ts +++ b/src/hooks/useAction.ts @@ -59,7 +59,7 @@ export function useAction({ } let ignore = false; - Action.fromApiUrl(actionApiUrl, supportStrategy) + Action.fetch(actionApiUrl, undefined, supportStrategy) .then((action) => { if (ignore) { return; diff --git a/src/ui/ActionContainer.tsx b/src/ui/ActionContainer.tsx index 4f3092bc..52f6bf12 100644 --- a/src/ui/ActionContainer.tsx +++ b/src/ui/ActionContainer.tsx @@ -4,6 +4,7 @@ import { Action, type ActionCallbacksConfig, type ActionContext, + type ActionPostResponse, type ActionSupportability, ButtonActionComponent, type ExtendedActionState, @@ -16,10 +17,6 @@ import { mergeActionStates, MultiValueActionComponent, SingleValueActionComponent, - type ActionCallbacksConfig, - type ActionContext, - type ActionPostResponse, - type ExtendedActionState, } from '../api'; import { checkSecurity, type SecurityLevel } from '../shared'; import { isInterstitial } from '../utils/interstitial-url.ts'; @@ -30,6 +27,7 @@ import { import { ActionLayout, type Disclaimer, + DisclaimerType, type StylePreset, } from './ActionLayout'; From 0a26db7c94819605990413b5cab33e53a4e8f5b7 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 14:44:14 +0300 Subject: [PATCH 17/24] fixes --- src/api/Action/Action.ts | 4 +-- src/api/Action/action-supportability.ts | 38 ++++++++++++++++++------- src/api/ActionConfig.ts | 7 ++--- src/ui/ActionLayout.tsx | 6 ++-- src/ui/icons/ConfigIcon.tsx | 1 + 5 files changed, 37 insertions(+), 19 deletions(-) diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index fb151273..2c2a9b8a 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -110,8 +110,8 @@ export class Action { return this._data.error?.message ?? null; } - public get metadata(): Required { - // TODO: Change fallback to baseline version after a few weeks after proxies adopt versioning and remove Required + public get metadata(): ActionMetadata { + // TODO: Remove fallback to baseline version after a few weeks after compatibility is adopted return { blockchainIds: this._metadata.blockchainIds ?? BASELINE_ACTION_BLOCKCHAIN_IDS, diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index ddac0283..f9027c8d 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -59,9 +59,23 @@ export const defaultActionSupportStrategy: ActionSupportStrategy = async ( ) => { const { version: actionVersion, blockchainIds: actionBlockchainIds } = action.metadata; + + // Will be displayed in the future once we remove backward compatibility fallbacks for blockchains and version + if ( + !actionVersion || + !actionBlockchainIds || + actionBlockchainIds.length === 0 + ) { + return { + isSupported: false, + message: + 'Action compatibility metadata is not set. Please contact the action provider.', + }; + } + const supportedActionVersion = MAX_SUPPORTED_ACTION_VERSION; const supportedBlockchainIds = !action.adapterUnsafe - ? action.metadata.blockchainIds // Assuming action is supported if this happens for optimistic compatibility + ? actionBlockchainIds // Assuming action is supported if adapter absent for optimistic compatibility : action.adapterUnsafe.metadata.supportedBlockchainIds; const versionSupported = isVersionSupported({ @@ -73,33 +87,37 @@ export const defaultActionSupportStrategy: ActionSupportStrategy = async ( supportedBlockchainIds, }); - const actionBlockchainNames = actionBlockchainIds.map( + const notSupportedBlockchainIds = actionBlockchainIds.filter( + (id) => !supportedBlockchainIds.includes(id), + ); + + const notSupportedActionBlockchainNames = notSupportedBlockchainIds.map( (id) => BlockchainNames[id] ?? id, ); if (!versionSupported && !blockchainSupported) { const blockchainMessage = - actionBlockchainIds.length === 1 - ? `blockchain ${actionBlockchainNames[0]}` - : `blockchains ${actionBlockchainNames.join(', ')}`; + notSupportedActionBlockchainNames.length === 1 + ? `blockchain ${notSupportedActionBlockchainNames[0]}` + : `blockchains ${notSupportedActionBlockchainNames.join(', ')}`; return { isSupported: false, - message: `Action version ${actionVersion} and ${blockchainMessage} are not supported by the Blink client.`, + message: `Action version ${actionVersion} and ${blockchainMessage} are not supported by your Blink client.`, }; } if (!versionSupported) { return { isSupported: false, - message: `Action version is not supported by the Blink client.`, + message: `Action version ${actionVersion} is not supported by your Blink client.`, }; } if (!blockchainSupported) { const blockchainMessage = - actionBlockchainIds.length === 1 - ? `Action blockchain ${actionBlockchainNames[0]} is not supported by the Blink client.` - : `Action blockchains ${actionBlockchainNames.join(', ')} are not supported by the Blink client.`; + notSupportedActionBlockchainNames.length === 1 + ? `Action blockchain ${notSupportedActionBlockchainNames[0]} is not supported by your Blink client.` + : `Action blockchains ${notSupportedActionBlockchainNames.join(', ')} are not supported by your Blink client.`; return { isSupported: false, diff --git a/src/api/ActionConfig.ts b/src/api/ActionConfig.ts index 8787dc95..03dcdf06 100644 --- a/src/api/ActionConfig.ts +++ b/src/api/ActionConfig.ts @@ -1,7 +1,7 @@ import { Connection } from '@solana/web3.js'; -import { BlockchainIds } from '../utils'; import { type Action } from './Action'; import { AbstractActionComponent } from './Action/action-components'; +import { DEFAULT_SUPPORTED_BLOCKCHAIN_IDS } from './Action/action-supportability.ts'; export interface ActionContext { originalUrl: string; @@ -46,10 +46,7 @@ export interface ActionAdapter { export class ActionConfig implements ActionAdapter { private static readonly CONFIRM_TIMEOUT_MS = 60000 * 1.2; // 20% extra time private static readonly DEFAULT_METADATA: ActionAdapterMetadata = { - supportedBlockchainIds: [ - BlockchainIds.SOLANA_MAINNET, - BlockchainIds.SOLANA_DEVNET, - ], + supportedBlockchainIds: DEFAULT_SUPPORTED_BLOCKCHAIN_IDS, }; private connection: Connection; diff --git a/src/ui/ActionLayout.tsx b/src/ui/ActionLayout.tsx index d86fb551..d5c8e947 100644 --- a/src/ui/ActionLayout.tsx +++ b/src/ui/ActionLayout.tsx @@ -104,10 +104,12 @@ const NotSupportedBlock = ({
- +
+ +
This action is not supported -

{message}.

+

{message}

diff --git a/src/ui/icons/ConfigIcon.tsx b/src/ui/icons/ConfigIcon.tsx index 7df17472..8719f2cb 100644 --- a/src/ui/icons/ConfigIcon.tsx +++ b/src/ui/icons/ConfigIcon.tsx @@ -6,6 +6,7 @@ const SvgComponent = (props: SVGProps) => ( height={16} fill="none" viewBox="0 0 16 16" + preserveAspectRatio="xMidYMid meet" {...props} > Date: Mon, 12 Aug 2024 16:04:00 +0300 Subject: [PATCH 18/24] set baseline action version to 2.2 --- src/api/Action/action-supportability.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index f9027c8d..2243a502 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -16,7 +16,7 @@ export const DEFAULT_SUPPORTED_BLOCKCHAIN_IDS = [ * Baseline action version to be used when not set by action provider. * Defaults to latest release that doesn't support versioning. */ -export const BASELINE_ACTION_VERSION = '2.0.0'; +export const BASELINE_ACTION_VERSION = '2.2'; /** * Baseline blockchain IDs to be used when not set by action provider. * Defaults to Solana mainnet. From 049e7c5e74ff4983f8b9004dab329c7da544063a Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 16:14:11 +0300 Subject: [PATCH 19/24] export supportability primitives --- src/api/Action/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/api/Action/index.ts b/src/api/Action/index.ts index 3d6d2abe..71373a1a 100644 --- a/src/api/Action/index.ts +++ b/src/api/Action/index.ts @@ -1,2 +1,3 @@ export * from './action-components'; +export * from './action-supportability.ts'; export * from './Action.ts'; From dfc29106dac94c18773390b672482f69d4be8c3a Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 18:14:18 +0300 Subject: [PATCH 20/24] don't check supportability for chained actions --- src/ui/ActionContainer.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ui/ActionContainer.tsx b/src/ui/ActionContainer.tsx index 52f6bf12..b3b52506 100644 --- a/src/ui/ActionContainer.tsx +++ b/src/ui/ActionContainer.tsx @@ -291,6 +291,9 @@ export const ActionContainer = ({ useEffect(() => { const checkSupportability = async (action: Action) => { + if (action.isChained) { + return; + } try { dispatch({ type: ExecutionType.CHECK_SUPPORTABILITY }); const supportability = await action.isSupported(); From eb05976f7fbc51fa397f68ab7c12283524412111 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 20:48:14 +0300 Subject: [PATCH 21/24] code review fixes --- src/api/Action/action-supportability.ts | 4 ++-- src/ui/ActionLayout.tsx | 8 ++++++-- src/utils/caip-2.ts | 22 ++++++++++++++++++++-- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/api/Action/action-supportability.ts b/src/api/Action/action-supportability.ts index 2243a502..cebad8e5 100644 --- a/src/api/Action/action-supportability.ts +++ b/src/api/Action/action-supportability.ts @@ -1,4 +1,4 @@ -import { BlockchainIds, BlockchainNames } from '../../utils/caip-2.ts'; +import { BlockchainIds, getShortBlockchainName } from '../../utils/caip-2.ts'; import { ACTIONS_SPEC_VERSION } from '../../utils/dependency-versions.ts'; import type { Action } from './Action.ts'; @@ -92,7 +92,7 @@ export const defaultActionSupportStrategy: ActionSupportStrategy = async ( ); const notSupportedActionBlockchainNames = notSupportedBlockchainIds.map( - (id) => BlockchainNames[id] ?? id, + getShortBlockchainName, ); if (!versionSupported && !blockchainSupported) { diff --git a/src/ui/ActionLayout.tsx b/src/ui/ActionLayout.tsx index d5c8e947..bd6a657b 100644 --- a/src/ui/ActionLayout.tsx +++ b/src/ui/ActionLayout.tsx @@ -102,7 +102,11 @@ const NotSupportedBlock = ({ }) => { return (
- +
@@ -112,7 +116,7 @@ const NotSupportedBlock = ({

{message}

- +
); }; diff --git a/src/utils/caip-2.ts b/src/utils/caip-2.ts index 1f739c56..2be918cf 100644 --- a/src/utils/caip-2.ts +++ b/src/utils/caip-2.ts @@ -7,10 +7,28 @@ export const BlockchainIds = { SOLANA_TESTNET: 'solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3', ETHEREUM_MAINNET: 'eip155:1', }; - -export const BlockchainNames: Record = { +const BlockchainNames: Record = { [BlockchainIds.SOLANA_MAINNET]: 'Solana Mainnet', [BlockchainIds.SOLANA_DEVNET]: 'Solana Devnet', [BlockchainIds.SOLANA_TESTNET]: 'Solana Testnet', [BlockchainIds.ETHEREUM_MAINNET]: 'Ethereum Mainnet', }; + +export function getShortBlockchainName(id: string) { + const blockchainName = BlockchainNames[id]; + if (!blockchainName) { + // https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md + // chain_id: namespace + ":" + reference + const [chainId, reference] = id.split(':'); + if (chainId && reference) { + // If the ID is in CAIP-2 format, truncate the reference to 3 characters + const truncatedReference = + reference.length > 3 ? reference.slice(0, 3) + '...' : reference; + return `${chainId}:${truncatedReference}`; + } else { + // If the ID is not in CAIP-2 format, truncate the entire ID to 8 characters + return id.length > 8 ? id.slice(0, 8) + '...' : id; + } + } + return blockchainName; +} From 681360cb1957bc1a13f61bad4fe3cf934a169df8 Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 20:48:33 +0300 Subject: [PATCH 22/24] remove info variant from snackbar --- src/ui/Snackbar.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/Snackbar.tsx b/src/ui/Snackbar.tsx index 83a3c5f7..0708acd3 100644 --- a/src/ui/Snackbar.tsx +++ b/src/ui/Snackbar.tsx @@ -1,7 +1,7 @@ import clsx from 'clsx'; import type { ReactNode } from 'react'; -type SnackbarVariant = 'warning' | 'error' | 'info'; +type SnackbarVariant = 'warning' | 'error'; interface Props { variant?: SnackbarVariant; @@ -11,7 +11,6 @@ interface Props { const variantClasses: Record = { error: 'bg-transparent-error text-text-error border-stroke-error', warning: 'bg-transparent-warning text-text-warning border-stroke-warning', - info: 'bg-bg-secondary text-text-secondary border-none', }; export const Snackbar = ({ variant = 'warning', children }: Props) => { From 1e537b5e0eed018412b77a068557331a362b1beb Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 20:50:23 +0300 Subject: [PATCH 23/24] cleanup --- src/ui/ActionContainer.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/ui/ActionContainer.tsx b/src/ui/ActionContainer.tsx index b3b52506..19c99780 100644 --- a/src/ui/ActionContainer.tsx +++ b/src/ui/ActionContainer.tsx @@ -306,11 +306,6 @@ export const ActionContainer = ({ checkSupportability(action); }, [action]); - // const supportability: ActionSupportability = useMemo(() => { - // const actionSupportability = action.isSupported(); - // return actionSupportability; - // }, [action]); - const buttons = useMemo( () => action?.actions From 4c20af9078e504f918bd3607562db056a86b725d Mon Sep 17 00:00:00 2001 From: Alexey Tsymbal Date: Mon, 12 Aug 2024 21:05:20 +0300 Subject: [PATCH 24/24] add error handling for supportability checks --- src/api/Action/Action.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/api/Action/Action.ts b/src/api/Action/Action.ts index 2c2a9b8a..dde3f81a 100644 --- a/src/api/Action/Action.ts +++ b/src/api/Action/Action.ts @@ -135,8 +135,19 @@ export class Action { this._adapter = adapter; } - public isSupported() { - return this._supportStrategy(this); + public async isSupported() { + try { + return await this._supportStrategy(this); + } catch (e) { + console.error( + `[@dialectlabs/blinks] Failed to check supportability for action ${this.url}`, + ); + return { + isSupported: false, + message: + 'Failed to check supportability, please contact your Blink client provider.', + }; + } } public async chain(