Skip to content

Compatibility of Blinks and Actions #16

New issue

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

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

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
b3d06e7
allow only patch version updates for @solana/actions-spec
tsmbl Aug 6, 2024
712c9d1
add util to extract @solana/actions-spec version
tsmbl Aug 6, 2024
34e0413
add todo in dependency version to replace it
tsmbl Aug 6, 2024
7bbf3b3
expose version in action metadata, make blockchain ids optional
tsmbl Aug 6, 2024
0dd884a
add action supportability utils
tsmbl Aug 6, 2024
b3813b8
add fallback ui on action not supported
tsmbl Aug 6, 2024
65105c9
cleanup
tsmbl Aug 6, 2024
ea3736a
add action metadata in action adapter
tsmbl Aug 7, 2024
60392e4
craft action metadata aware message on action unsupported
tsmbl Aug 7, 2024
5059196
set baseline version to 2.0.0
tsmbl Aug 8, 2024
a8ba440
add todo to remove defaults
tsmbl Aug 8, 2024
997fe32
WIP move compatibility strategy outside of ActionAdapter
tsmbl Aug 9, 2024
c30b3d4
implement async compatibility check inside action container
tsmbl Aug 9, 2024
2b4491c
better human readable messages on errors
tsmbl Aug 9, 2024
eda8dd0
add ethereum mainnet to caip-2 constants
tsmbl Aug 10, 2024
b286a50
Merge branch 'refs/heads/main' into feat/action-compatibility
tsmbl Aug 12, 2024
907d88a
merge with main
tsmbl Aug 12, 2024
0a26db7
fixes
tsmbl Aug 12, 2024
46fc4a2
set baseline action version to 2.2
tsmbl Aug 12, 2024
049e7c5
export supportability primitives
tsmbl Aug 12, 2024
dfc2910
don't check supportability for chained actions
tsmbl Aug 12, 2024
eb05976
code review fixes
tsmbl Aug 12, 2024
681360c
remove info variant from snackbar
tsmbl Aug 12, 2024
1e537b5
cleanup
tsmbl Aug 12, 2024
4c20af9
add error handling for supportability checks
tsmbl Aug 12, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
97 changes: 78 additions & 19 deletions src/api/Action/Action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,18 @@ import {
MultiValueActionComponent,
SingleValueActionComponent,
} from './action-components';
import {
type ActionSupportStrategy,
BASELINE_ACTION_BLOCKCHAIN_IDS,
BASELINE_ACTION_VERSION,
defaultActionSupportStrategy,
} from './action-supportability.ts';

const MULTI_VALUE_TYPES: ActionParameterType[] = ['checkbox'];

interface ActionMetadata {
blockchainIds: string[];
blockchainIds?: string[];
version?: string;
}

type ActionChainMetadata =
Expand All @@ -40,6 +47,7 @@ export class Action {
private readonly _url: string,
private readonly _data: NextAction,
private readonly _metadata: ActionMetadata,
private readonly _supportStrategy: ActionSupportStrategy,
private _adapter?: ActionAdapter,
private readonly _chainMetadata: ActionChainMetadata = { isChained: false },
) {
Expand Down Expand Up @@ -102,8 +110,17 @@ export class Action {
return this._data.error?.message ?? null;
}

public get metadata() {
return this._metadata;
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,
version: this._metadata.version ?? BASELINE_ACTION_VERSION,
};
}

public get adapterUnsafe() {
return this._adapter;
}

public get adapter() {
Expand All @@ -118,15 +135,37 @@ export class Action {
this._adapter = adapter;
}

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<N extends NextActionLink>(
next: N,
chainData?: N extends PostNextActionLink ? NextActionPostRequest : never,
): Promise<Action | null> {
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);
Expand Down Expand Up @@ -161,23 +200,35 @@ 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
static hydrate(
url: string,
data: NextAction,
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 fetch(
apiUrl: string,
adapter?: ActionAdapter,
supportStrategy: ActionSupportStrategy = defaultActionSupportStrategy,
) {
const proxyUrl = proxify(apiUrl);
const response = await fetch(proxyUrl, {
headers: {
Expand All @@ -194,19 +245,27 @@ 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,
);
}
}

const getActionMetadata = (response: Response): ActionMetadata => {
// 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();

return {
blockchainIds,
} satisfies ActionMetadata;
version,
};
};

const componentFactory = (
Expand Down
183 changes: 183 additions & 0 deletions src/api/Action/action-supportability.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import { BlockchainIds, getShortBlockchainName } from '../../utils/caip-2.ts';
import { ACTIONS_SPEC_VERSION } from '../../utils/dependency-versions.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.
*/
export const BASELINE_ACTION_VERSION = '2.2';
/**
* Baseline blockchain IDs to be used when not set by action provider.
* Defaults to Solana mainnet.
*/
export const BASELINE_ACTION_BLOCKCHAIN_IDS = [BlockchainIds.SOLANA_MAINNET];

type IsVersionSupportedParams = {
actionVersion: string;
supportedActionVersion: string;
};

type IsBlockchainIdSupportedParams = {
actionBlockchainIds: string[];
supportedBlockchainIds: string[];
};

export type ActionSupportability =
| {
isSupported: true;
}
| {
isSupported: false;
message: string;
};

export type ActionSupportStrategy = (
action: Action,
) => Promise<ActionSupportability>;

/**
* Default implementation for checking if an action is supported.
* Checks if the action version and the action blockchain IDs are supported by blink.
* @param action Action.
*
* @see {isVersionSupported}
* @see {isBlockchainSupported}
*/
export const defaultActionSupportStrategy: ActionSupportStrategy = async (
action,
) => {
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
? actionBlockchainIds // Assuming action is supported if adapter absent for optimistic compatibility
: action.adapterUnsafe.metadata.supportedBlockchainIds;

const versionSupported = isVersionSupported({
actionVersion,
supportedActionVersion,
});
const blockchainSupported = isBlockchainSupported({
actionBlockchainIds,
supportedBlockchainIds,
});

const notSupportedBlockchainIds = actionBlockchainIds.filter(
(id) => !supportedBlockchainIds.includes(id),
);

const notSupportedActionBlockchainNames = notSupportedBlockchainIds.map(
getShortBlockchainName,
);

if (!versionSupported && !blockchainSupported) {
const blockchainMessage =
notSupportedActionBlockchainNames.length === 1
? `blockchain ${notSupportedActionBlockchainNames[0]}`
: `blockchains ${notSupportedActionBlockchainNames.join(', ')}`;
return {
isSupported: false,
message: `Action version ${actionVersion} and ${blockchainMessage} are not supported by your Blink client.`,
};
}

if (!versionSupported) {
return {
isSupported: false,
message: `Action version ${actionVersion} is not supported by your Blink client.`,
};
}

if (!blockchainSupported) {
const blockchainMessage =
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,
message: blockchainMessage,
};
}
return {
isSupported: true,
};
};

/**
* 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({
supportedActionVersion,
actionVersion,
}: IsVersionSupportedParams): boolean {
return compareSemverIgnoringPatch(actionVersion, supportedActionVersion) <= 0;
}

function compareSemverIgnoringPatch(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;
}

/**
* 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({
supportedBlockchainIds,
actionBlockchainIds,
}: IsBlockchainIdSupportedParams): boolean {
if (actionBlockchainIds.length === 0 || supportedBlockchainIds.length === 0) {
return false;
}
const sanitizedSupportedBlockchainIds = supportedBlockchainIds.map((it) =>
it.trim(),
);
const sanitizedActionBlockchainIds = actionBlockchainIds.map((it) =>
it.trim(),
);
return sanitizedActionBlockchainIds.every((chain) =>
sanitizedSupportedBlockchainIds.includes(chain),
);
}
1 change: 1 addition & 0 deletions src/api/Action/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './action-components';
export * from './action-supportability.ts';
export * from './Action.ts';
Loading