Skip to content

Feature/spec 2.2 chaining #17

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 6 commits into from
Aug 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
111 changes: 95 additions & 16 deletions src/api/Action/Action.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { isUrlSameOrigin } from '../../shared';
import { proxify, proxifyImage } from '../../utils/proxify.ts';
import type { ActionAdapter } from '../ActionConfig.ts';
import type {
ActionGetResponse,
ActionParameter,
ActionParameterType,
NextAction,
NextActionLink,
NextActionPostRequest,
PostNextActionLink,
TypedActionParameter,
} from '../actions-spec.ts';
import {
type AbstractActionComponent,
Expand All @@ -19,17 +24,27 @@ interface ActionMetadata {
blockchainIds: string[];
}

type ActionChainMetadata =
| {
isChained: true;
isInline: boolean;
}
| {
isChained: false;
};

export class Action {
private readonly _actions: AbstractActionComponent[];

private constructor(
private readonly _url: string,
private readonly _data: ActionGetResponse,
private readonly _data: NextAction,
private readonly _metadata: ActionMetadata,
private _adapter?: ActionAdapter,
private readonly _chainMetadata: ActionChainMetadata = { isChained: false },
) {
// if no links present, fallback to original solana pay spec
if (!_data.links?.actions) {
// if no links present or completed, fallback to original solana pay spec (or just using the button as a placeholder)
if (_data.type === 'completed' || !_data.links?.actions) {
this._actions = [new ButtonActionComponent(this, _data.label, _url)];
return;
}
Expand All @@ -44,6 +59,18 @@ export class Action {
});
}

public get isChained() {
return this._chainMetadata.isChained;
}

public get isInline() {
return this._chainMetadata.isChained ? this._chainMetadata.isInline : false;
}

public get type() {
return this._data.type;
}

public get url() {
return this._url;
}
Expand Down Expand Up @@ -91,10 +118,59 @@ export class Action {
this._adapter = adapter;
}

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,
});
}

const baseUrlObj = new URL(this.url);

if (!isUrlSameOrigin(baseUrlObj.origin, next.href)) {
console.error(
`Chained action is not the same origin as the current action. Original: ${this.url}, chained: ${next.href}`,
);
return null;
}

const href = next.href.startsWith('http')
? next.href
: baseUrlObj.origin + next.href;

const proxyUrl = proxify(href);
const response = await fetch(proxyUrl, {
method: 'POST',
body: JSON.stringify(chainData),
headers: {
Accept: 'application/json',
},
});

if (!response.ok) {
console.error(
`Failed to fetch chained action ${proxyUrl}, action url: ${next.href}`,
);
return null;
}

const data = (await response.json()) as NextAction;
const metadata = getActionMetadata(response);

return new Action(href, data, metadata, this.adapter, {
isChained: true,
isInline: false,
});
}

// be sure to use this only if the action is valid
static hydrate(
url: string,
data: ActionGetResponse,
data: NextAction,
metadata: ActionMetadata,
adapter?: ActionAdapter,
) {
Expand All @@ -116,25 +192,28 @@ export class Action {
}

const data = (await response.json()) as ActionGetResponse;
const metadata = getActionMetadata(response);

// for multi-chain x-blockchain-ids
const blockchainIds = (
response?.headers?.get('x-blockchain-ids') || ''
).split(',');

const metadata: ActionMetadata = {
blockchainIds,
};

return new Action(apiUrl, data, metadata, adapter);
return new Action(apiUrl, { ...data, type: 'action' }, metadata, adapter);
}
}

const getActionMetadata = (response: Response): ActionMetadata => {
// for multi-chain x-blockchain-ids
const blockchainIds = (
response?.headers?.get('x-blockchain-ids') || ''
).split(',');

return {
blockchainIds,
} satisfies ActionMetadata;
};

const componentFactory = (
parent: Action,
label: string,
href: string,
parameters?: ActionParameter<ActionParameterType>[],
parameters?: TypedActionParameter[],
): AbstractActionComponent => {
if (!parameters?.length) {
return new ButtonActionComponent(parent, label, href);
Expand Down
5 changes: 2 additions & 3 deletions src/api/Action/action-components/AbstractActionComponent.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { proxify } from '../../../utils/proxify.ts';
import type {
ActionError,
ActionParameter,
ActionParameterType,
ActionPostRequest,
ActionPostResponse,
TypedActionParameter,
} from '../../actions-spec.ts';
import { Action } from '../Action.ts';

Expand All @@ -13,7 +12,7 @@ export abstract class AbstractActionComponent {
protected _parent: Action,
protected _label: string,
protected _href: string,
protected _parameters?: ActionParameter<ActionParameterType>[],
protected _parameters?: TypedActionParameter[],
) {}

public get parent() {
Expand Down
5 changes: 2 additions & 3 deletions src/api/Action/action-components/ButtonActionComponent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
ActionParameter,
ActionParameterType,
ActionPostRequest,
TypedActionParameter,
} from '../../actions-spec.ts';
import { Action } from '../Action.ts';
import { AbstractActionComponent } from './AbstractActionComponent.ts';
Expand All @@ -11,7 +10,7 @@ export class ButtonActionComponent extends AbstractActionComponent {
protected _parent: Action,
protected _label: string,
protected _href: string,
protected _parameters?: ActionParameter<ActionParameterType>[],
protected _parameters?: TypedActionParameter[],
protected _parentComponent?: AbstractActionComponent,
) {
super(_parent, _label, _href, _parameters);
Expand Down
5 changes: 2 additions & 3 deletions src/api/Action/action-components/FormActionComponent.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
ActionParameter,
ActionParameterType,
ActionPostRequest,
TypedActionParameter,
} from '../../actions-spec.ts';
import { Action } from '../Action.ts';
import { AbstractActionComponent } from './AbstractActionComponent.ts';
Expand All @@ -15,7 +14,7 @@ export class FormActionComponent extends AbstractActionComponent {
protected _parent: Action,
protected _label: string,
protected _href: string,
protected _parameters?: ActionParameter<ActionParameterType>[],
protected _parameters?: TypedActionParameter[],
protected _parentComponent?: AbstractActionComponent,
) {
super(_parent, _label, _href, _parameters);
Expand Down
9 changes: 4 additions & 5 deletions src/api/Action/action-components/MultiValueActionComponent.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import type {
ActionParameter,
ActionParameterType,
ActionPostRequest,
SelectableParameterType,
TypedActionParameter,
} from '../../actions-spec.ts';
import { Action } from '../Action.ts';
import { AbstractActionComponent } from './AbstractActionComponent.ts';
Expand All @@ -15,7 +14,7 @@ export class MultiValueActionComponent extends AbstractActionComponent {
protected _parent: Action,
protected _label: string,
protected _href: string,
protected _parameters?: ActionParameter<ActionParameterType>[],
protected _parameters?: TypedActionParameter[],
protected _parentComponent?: AbstractActionComponent,
) {
super(_parent, _label, _href, _parameters);
Expand Down Expand Up @@ -45,10 +44,10 @@ export class MultiValueActionComponent extends AbstractActionComponent {
return this.parameter.type === 'checkbox';
}

public get parameter(): ActionParameter<SelectableParameterType> {
public get parameter(): TypedActionParameter<SelectableParameterType> {
const [param] = this.parameters;

return param as ActionParameter<SelectableParameterType>;
return param as TypedActionParameter<SelectableParameterType>;
}

public setValue(value: string | Array<string>) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {
ActionParameter,
ActionParameterType,
GeneralParameterType,
TypedActionParameter,
} from '../../actions-spec.ts';
import { Action } from '../Action.ts';
import { AbstractActionComponent } from './AbstractActionComponent.ts';
Expand All @@ -14,7 +13,7 @@ export class SingleValueActionComponent extends AbstractActionComponent {
protected _parent: Action,
protected _label: string,
protected _href: string,
protected _parameters?: ActionParameter<ActionParameterType>[],
protected _parameters?: TypedActionParameter[],
protected _parentComponent?: AbstractActionComponent,
) {
super(_parent, _label, _href, _parameters);
Expand All @@ -37,10 +36,10 @@ export class SingleValueActionComponent extends AbstractActionComponent {
};
}

public get parameter(): ActionParameter<GeneralParameterType> {
public get parameter(): TypedActionParameter<GeneralParameterType> {
const [param] = this.parameters;

return param as ActionParameter<GeneralParameterType>;
return param as TypedActionParameter<GeneralParameterType>;
}

public setValue(value: string) {
Expand Down
12 changes: 4 additions & 8 deletions src/api/Action/action-components/guards.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
import type {
ActionParameter,
ActionParameterSelectable,
ActionParameterType,
SelectableParameterType,
TypedActionParameter,
} from '../../actions-spec.ts';

export const isPatternAllowed = (
parameter: ActionParameter<ActionParameterType>,
) => {
export const isPatternAllowed = (parameter: TypedActionParameter) => {
return (
parameter.type !== 'select' &&
parameter.type !== 'radio' &&
Expand All @@ -16,8 +12,8 @@ export const isPatternAllowed = (
};

export const isParameterSelectable = (
parameter: ActionParameter<ActionParameterType>,
): parameter is ActionParameterSelectable<SelectableParameterType> => {
parameter: TypedActionParameter,
): parameter is TypedActionParameter<SelectableParameterType> => {
return (
parameter.type === 'select' ||
parameter.type === 'radio' ||
Expand Down
8 changes: 6 additions & 2 deletions src/api/ActionConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export class ActionConfig implements ActionAdapter {

const confirm = async () => {
if (Date.now() - start >= ActionConfig.CONFIRM_TIMEOUT_MS) {
rej(new Error('Unable to confirm transaction'));
rej(new Error('Unable to confirm transaction: timeout reached'));
return;
}

Expand All @@ -74,7 +74,11 @@ export class ActionConfig implements ActionAdapter {

// if error present, transaction failed
if (status.value?.err) {
rej(new Error('Transaction execution failed'));
rej(
new Error(
`Transaction execution failed, check wallet for details`,
),
);
return;
}

Expand Down
Loading