Skip to content
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

Support for Account Creation Plugins #84

Merged
merged 21 commits into from
Nov 10, 2023
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@wharfkit/session",
"description": "Create account-based sessions, perform transactions, and allow users to login using Antelope-based blockchains.",
"version": "1.1.0-rc2",
"version": "1.1.0-rc4",
"homepage": "https://github.com/wharfkit/session",
"license": "BSD-3-Clause",
"main": "lib/session.js",
Expand Down
154 changes: 154 additions & 0 deletions src/account-creation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {APIClient, FetchProvider, NameType, Struct} from '@wharfkit/antelope'
import {Logo} from '@wharfkit/common'
import type {ChainDefinition, Fetch, LocaleDefinitions} from '@wharfkit/common'
import {UserInterface} from '.'

/**
* The static configuration of an [[AccountCreationPlugin]].
*/
export interface AccountCreationPluginConfig {
/**
* Indicates if the plugin requires the user to manually select the blockchain to create an account on.
*/
requiresChainSelect: boolean
/**
* If set, indicates which blockchains are compatible with this [[AccountCreationPlugin]].
*/
supportedChains?: ChainDefinition[]
}

/**
* The metadata of an [[AccountCreationPlugin]].
*/
@Struct.type('account_creation_plugin_metadata')
export class AccountCreationPluginMetadata extends Struct {
/**
* A display name for the account creation service that is presented to users.
*/
@Struct.field('string') declare name: string
/**
* A description to further identify the account creation service for users.
*/
@Struct.field('string', {optional: true}) declare description?: string
/**
* Account creation service branding.
*/
@Struct.field(Logo, {optional: true}) declare logo?: Logo
/**
* Link to the homepage for the account creation service.
*/
@Struct.field('string', {optional: true}) declare homepage?: string

static from(data) {
return new AccountCreationPluginMetadata({
...data,
logo: data.logo ? Logo.from(data.logo) : undefined,
})
}
}

/**
* Options for createAccount call.
**/
export interface CreateAccountOptions {
accountName?: NameType
chain?: ChainDefinition
pluginId?: string
}

/**
* The response for a createAccount call.
*/
export interface CreateAccountResponse {
chain: ChainDefinition
accountName: NameType
}

export interface CreateAccountContextOptions {
accountCreationPlugins?: AccountCreationPlugin[]
appName?: NameType
// client: APIClient
chain?: ChainDefinition
chains?: ChainDefinition[]
fetch: Fetch
ui: UserInterface
uiRequirements?: UserInterfaceAccountCreationRequirements
}

export interface UserInterfaceAccountCreationRequirements {
requiresChainSelect: boolean
requiresPluginSelect: boolean
}

export class CreateAccountContext {
accountCreationPlugins: AccountCreationPlugin[] = []
appName?: string
chain?: ChainDefinition
chains?: ChainDefinition[]
fetch: Fetch
ui: UserInterface
uiRequirements: UserInterfaceAccountCreationRequirements = {
requiresChainSelect: true,
requiresPluginSelect: true,
}

constructor(options: CreateAccountContextOptions) {
this.appName = String(options.appName)
if (options.chains) {
this.chains = options.chains
}
if (options.chain) {
this.chain = options.chain
}
this.fetch = options.fetch
this.ui = options.ui
if (options.accountCreationPlugins) {
this.accountCreationPlugins = options.accountCreationPlugins
}
if (options.uiRequirements) {
this.uiRequirements = options.uiRequirements
}
}

getClient(chain: ChainDefinition): APIClient {
return new APIClient({provider: new FetchProvider(chain.url, {fetch: this.fetch})})
}
}

/**
* Interface which all 3rd party account creation plugins must implement.
*/

export interface AccountCreationPlugin {
/** A URL friendly (lower case, no spaces, etc) ID for this plugin - Used in serialization */
get id(): string

/** A display name for the account creation service that is presented to users. */
get name(): string

/** The [[SessionKit]] configuration parameters for this [[WalletPlugin]]. */
config: AccountCreationPluginConfig
/** Any translations this plugin requires */
translations?: LocaleDefinitions

/**
* Request the [[AccountCreationPlugin]] to create a new account.
*
* @param context The [[AccountCreationContext]] for the [[WalletPlugin]] to use.
*/
create(options: CreateAccountContext): Promise<CreateAccountResponse>
}

/**
* Abstract class which all 3rd party [[AccountCreation]] implementations may extend.
*/
export abstract class AbstractAccountCreationPlugin implements AccountCreationPlugin {
config: AccountCreationPluginConfig = {
requiresChainSelect: true,
}
metadata: AccountCreationPluginMetadata = new AccountCreationPluginMetadata({})
translations?: LocaleDefinitions
abstract get id(): string
abstract get name(): string
abstract create(options: CreateAccountOptions): Promise<CreateAccountResponse>
}
1 change: 1 addition & 0 deletions src/index-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export * from './transact'
export * from './ui'
export * from './utils'
export * from './wallet'
export * from './account-creation'
150 changes: 147 additions & 3 deletions src/kit.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {ChainDefinitionType, Fetch} from '@wharfkit/common'
import {ChainDefinition, type ChainDefinitionType, type Fetch} from '@wharfkit/common'
import type {Contract} from '@wharfkit/contract'
import {
Checksum256,
Expand All @@ -8,7 +8,6 @@ import {
PermissionLevel,
PermissionLevelType,
} from '@wharfkit/antelope'
import {ChainDefinition} from '@wharfkit/common'

import {
AbstractLoginPlugin,
Expand All @@ -29,6 +28,12 @@ import {
import {WalletPlugin, WalletPluginLoginResponse, WalletPluginMetadata} from './wallet'
import {UserInterface} from './ui'
import {getFetch} from './utils'
import {
AccountCreationPlugin,
CreateAccountContext,
CreateAccountOptions,
CreateAccountResponse,
} from './account-creation'

export interface LoginOptions {
chain?: ChainDefinition | Checksum256Type
Expand Down Expand Up @@ -71,6 +76,7 @@ export interface SessionKitOptions {
storage?: SessionStorage
transactPlugins?: TransactPlugin[]
transactPluginsOptions?: TransactPluginsOptions
accountCreationPlugins?: AccountCreationPlugin[]
}

/**
Expand All @@ -89,6 +95,7 @@ export class SessionKit {
readonly transactPluginsOptions: TransactPluginsOptions = {}
readonly ui: UserInterface
readonly walletPlugins: WalletPlugin[]
readonly accountCreationPlugins: AccountCreationPlugin[] = []

constructor(args: SessionKitArgs, options: SessionKitOptions = {}) {
// Save the appName to the SessionKit instance
Expand Down Expand Up @@ -142,18 +149,155 @@ export class SessionKit {
if (options.transactPluginsOptions) {
this.transactPluginsOptions = options.transactPluginsOptions
}

// Establish default plugins for account creation
if (options.accountCreationPlugins) {
this.accountCreationPlugins = options.accountCreationPlugins
}
}

getChainDefinition(id: Checksum256Type, override?: ChainDefinition[]): ChainDefinition {
const chains = override ? override : this.chains
const chainId = Checksum256.from(id)
const chain = chains.find((c) => c.id.equals(chainId))
if (!chain) {
throw new Error(`No chain defined with the ID of: ${chainId}`)
throw new Error(`No chain defined with an ID of: ${chainId}`)
}
return chain
}

/**
* Request account creation.
*/
async createAccount(options?: CreateAccountOptions): Promise<CreateAccountResponse> {
try {
if (this.accountCreationPlugins.length === 0) {
throw new Error('No account creation plugins available.')
}

// Eestablish defaults based on options
let chain = options?.chain
let requiresChainSelect = !chain
let requiresPluginSelect = !options?.pluginId

let accountCreationPlugin: AccountCreationPlugin | undefined

// Developer specified a plugin during createAccount call
if (options?.pluginId) {
requiresPluginSelect = false

// Find the plugin
accountCreationPlugin = this.accountCreationPlugins.find(
(p) => p.id === options.pluginId
)

// Ensure the plugin exists
if (!accountCreationPlugin) {
throw new Error('Invalid account creation plugin selected.')
}

// Override the chain selection requirement based on the plugin
if (accountCreationPlugin?.config.requiresChainSelect !== undefined) {
requiresChainSelect = accountCreationPlugin?.config.requiresChainSelect
}

// If the plugin does not require chain select and has one supported chain, set it as the default
if (
!accountCreationPlugin.config.requiresChainSelect &&
accountCreationPlugin.config.supportedChains &&
accountCreationPlugin.config.supportedChains.length === 1
) {
chain = accountCreationPlugin.config.supportedChains[0]
}
}

// The chains available to select from, based on the Session Kit
let chains = this.chains

// If a plugin is selected, filter the chains available down to only the ones supported by the plugin
if (accountCreationPlugin && accountCreationPlugin?.config.supportedChains?.length) {
chains = chains.filter((availableChain) => {
return accountCreationPlugin?.config.supportedChains?.find((c) => {
return c.id.equals(availableChain.id)
})
})
}

const context = new CreateAccountContext({
accountCreationPlugins: this.accountCreationPlugins,
appName: this.appName,
chain,
chains,
fetch: this.fetch,
ui: this.ui,
uiRequirements: {
requiresChainSelect,
requiresPluginSelect,
},
})

// If UI interaction is required before triggering the plugin
if (requiresPluginSelect || requiresChainSelect) {
// Call the UI with the context
const response = await context.ui.onAccountCreate(context)

// Set pluginId based on options first, then response
const pluginId = options?.pluginId || response.pluginId

// Ensure we have a pluginId
if (!pluginId) {
throw new Error('No account creation plugin selected.')
}

// Determine plugin selected based on response
accountCreationPlugin = context.accountCreationPlugins.find(
(p) => p.id === pluginId
)
if (!accountCreationPlugin) {
throw new Error('No account creation plugin selected.')
}

// If the plugin does not require chain select and has one supported chain, set it as the default
if (
!accountCreationPlugin.config.requiresChainSelect &&
accountCreationPlugin.config.supportedChains &&
accountCreationPlugin.config.supportedChains.length === 1
) {
context.chain = accountCreationPlugin.config.supportedChains[0]
}

// Set chain based on response
if (response.chain) {
context.chain = this.getChainDefinition(response.chain, context.chains)
}

// Ensure a chain was selected and is supported by the plugin
if (accountCreationPlugin.config.requiresChainSelect && !context.chain) {
throw new Error(
`Account creation plugin (${pluginId}) requires chain selection, and no chain was selected.`
)
}
}

// Ensure a plugin was selected
if (!accountCreationPlugin) {
throw new Error('No account creation plugin selected')
}

// Call the account creation plugin with the context
const accountCreationData = await accountCreationPlugin.create(context)

// Notify the UI we're done
await context.ui.onAccountCreateComplete()

// Return the data
return accountCreationData
} catch (error: any) {
await this.ui.onError(error)
throw new Error(error)
}
}

/**
* Request a session from an account.
*
Expand Down
Loading
Loading