From 868f5b495dcbf19abeb592524139642068d11fb9 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 10 Jan 2023 13:18:50 -0500 Subject: [PATCH 01/26] Removal of the beforeBroadcast hook. Resolves #13. After discussions with other developers, it became clear that the `afterSign` and `beforeBroadcast` hooks currently serve the same function. To simplify the approach developers can take for plugin development, the `beforeBroadcast` hook was removed, and if required, can be added back at a later date. --- src/session.ts | 14 +++----------- test/tests/hooks.ts | 2 -- test/tests/plugins/hooks/beforeBroadcast.ts | 7 ------- test/tests/transact.ts | 1 - test/utils/mock-hook.ts | 1 - 5 files changed, 3 insertions(+), 22 deletions(-) delete mode 100644 test/tests/plugins/hooks/beforeBroadcast.ts diff --git a/src/session.ts b/src/session.ts index 9cb4fef1..286a042e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -27,7 +27,6 @@ export type TransactPluginsOptions = Record export enum TransactHookTypes { beforeSign = 'beforeSign', afterSign = 'afterSign', - beforeBroadcast = 'beforeBroadcast', afterBroadcast = 'afterBroadcast', } @@ -40,7 +39,6 @@ export interface TransactHooks { afterSign: TransactHook[] beforeSign: TransactHook[] afterBroadcast: TransactHook[] - beforeBroadcast: TransactHook[] } export interface TransactHookResponse { @@ -96,7 +94,6 @@ export class TransactContext { readonly hooks: TransactHooks = { afterBroadcast: [], afterSign: [], - beforeBroadcast: [], beforeSign: [], } readonly session: PermissionLevel @@ -328,10 +325,9 @@ export class Session { * A((Transact)) --> B{{"Hook(s): beforeSign"}} * B --> C[Wallet Plugin] * C --> D{{"Hook(s): afterSign"}} - * D --> E{{"Hook(s): beforeBroadcast"}} - * E --> F[Broadcast Plugin] - * F --> G{{"Hook(s): afterBroadcast"}} - * G --> H[TransactResult] + * D --> F[Broadcast Plugin] + * E --> G{{"Hook(s): afterBroadcast"}} + * F --> H[TransactResult] */ async transact(args: TransactArgs, options?: TransactOptions): Promise { // The context for this transaction @@ -398,10 +394,6 @@ export class Session { // Broadcast transaction if requested if (willBroadcast) { - // Run the `beforeBroadcast` hooks - for (const hook of context.hooks.beforeBroadcast) - await hook(result.request.clone(), context) - // Assemble the signed transaction to broadcast const signed = SignedTransaction.from({ ...result.resolved.transaction, diff --git a/test/tests/hooks.ts b/test/tests/hooks.ts index 22a6edc8..1ce9fd6b 100644 --- a/test/tests/hooks.ts +++ b/test/tests/hooks.ts @@ -1,6 +1,5 @@ import {beforeSignHooks} from './plugins/hooks/beforeSign' import {afterSignHooks} from './plugins/hooks/afterSign' -import {beforeBroadcastHooks} from './plugins/hooks/beforeBroadcast' import {afterBroadcastHooks} from './plugins/hooks/afterBroadcast' import {beforeLoginHooks} from './plugins/hooks/beforeLogin' import {afterLoginHooks} from './plugins/hooks/afterLogin' @@ -10,7 +9,6 @@ suite('hooks', function () { suite('transactHooks', function () { beforeSignHooks() afterSignHooks() - beforeBroadcastHooks() afterBroadcastHooks() }) // Perform login hook tests diff --git a/test/tests/plugins/hooks/beforeBroadcast.ts b/test/tests/plugins/hooks/beforeBroadcast.ts deleted file mode 100644 index 0e5a813f..00000000 --- a/test/tests/plugins/hooks/beforeBroadcast.ts +++ /dev/null @@ -1,7 +0,0 @@ -import {assert} from 'chai' - -export const beforeBroadcastHooks = () => { - suite('beforeBroadcast', function () { - test('TODO', async function () {}) - }) -} diff --git a/test/tests/transact.ts b/test/tests/transact.ts index 0352915c..13d3c2b0 100644 --- a/test/tests/transact.ts +++ b/test/tests/transact.ts @@ -284,7 +284,6 @@ suite('transact', function () { register(context) { context.addHook(TransactHookTypes.beforeSign, debugHook) context.addHook(TransactHookTypes.afterSign, debugHook) - context.addHook(TransactHookTypes.beforeBroadcast, debugHook) context.addHook(TransactHookTypes.afterBroadcast, debugHook) }, } diff --git a/test/utils/mock-hook.ts b/test/utils/mock-hook.ts index 89a9dc81..97ae7f2c 100644 --- a/test/utils/mock-hook.ts +++ b/test/utils/mock-hook.ts @@ -27,7 +27,6 @@ export class MockTransactPlugin extends AbstractTransactPlugin { register(context: TransactContext): void { context.addHook(TransactHookTypes.beforeSign, mockTransactHook) context.addHook(TransactHookTypes.afterSign, mockTransactHook) - context.addHook(TransactHookTypes.beforeBroadcast, mockTransactHook) context.addHook(TransactHookTypes.afterBroadcast, mockTransactHook) } } From 731246a755562e67810b9e3d12df9c7764ddc94d Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 10 Jan 2023 15:50:15 -0500 Subject: [PATCH 02/26] Added default esrOptions to the TransactContext Resolves #15. This gives developers a default set of options when creating ESR requests. --- src/session.ts | 24 ++++++++++++++ test/tests/context.ts | 31 ++++++++++++++++--- .../plugins/transact/resource-provider.ts | 17 +--------- 3 files changed, 52 insertions(+), 20 deletions(-) diff --git a/src/session.ts b/src/session.ts index 286a042e..8e3bd260 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,4 +1,5 @@ import { + ABI, ABIDef, AnyAction, AnyTransaction, @@ -16,6 +17,7 @@ import { ResolvedSigningRequest, ResolvedTransaction, SigningRequest, + SigningRequestEncodingOptions, } from 'eosio-signing-request' import zlib from 'pako' @@ -98,6 +100,7 @@ export class TransactContext { } readonly session: PermissionLevel readonly transactPluginsOptions: TransactPluginsOptions + constructor(options: TransactContextOptions) { this.client = options.client this.fetch = options.fetch @@ -107,6 +110,27 @@ export class TransactContext { plugin.register(this) }) } + + get abiProvider(): AbiProvider { + return { + getAbi: async (account: Name): Promise => { + const response = await this.client.v1.chain.get_abi(account) + if (!response.abi) { + /* istanbul ignore next */ + throw new Error('could not load abi') // TODO: Better coverage for this + } + return ABI.from(response.abi) + }, + } + } + + get esrOptions(): SigningRequestEncodingOptions { + return { + abiProvider: this.abiProvider, + zlib, + } + } + addHook(t: TransactHookTypes, hook: TransactHook) { this.hooks[t].push(hook) } diff --git a/test/tests/context.ts b/test/tests/context.ts index f5e67c14..52581e76 100644 --- a/test/tests/context.ts +++ b/test/tests/context.ts @@ -1,9 +1,32 @@ -import {makeClient} from '$test/utils/mock-client' +import {assert} from 'chai' -const client = makeClient() +import {ABI, Name} from '@greymass/eosio' +import zlib from 'pako' + +import {makeContext} from '$test/utils/mock-context' + +const context = makeContext() suite('context', function () { - suite('pre-sign', function () { - test('prepend action on `action`', async function () {}) + suite('abiProvider', function () { + test('has default', function () { + assert.isDefined(context.abiProvider) + }) + test('fetches ABIs', async function () { + const result = await context.abiProvider.getAbi(Name.from('eosio.token')) + const abi = ABI.from(result) + assert.instanceOf(result, ABI) + assert.equal(abi.version, 'eosio::abi/1.2') + }) + }) + suite('esrOptions', function () { + test('has abiProvider', function () { + assert.isDefined(context.esrOptions.abiProvider) + assert.hasAllKeys(context.esrOptions.abiProvider, ['getAbi']) + }) + test('has zlib', function () { + assert.isDefined(context.esrOptions.zlib) + assert.instanceOf(context.esrOptions.zlib, Object) + }) }) }) diff --git a/test/tests/plugins/transact/resource-provider.ts b/test/tests/plugins/transact/resource-provider.ts index 2576284b..117eb1ab 100644 --- a/test/tests/plugins/transact/resource-provider.ts +++ b/test/tests/plugins/transact/resource-provider.ts @@ -125,25 +125,10 @@ export class MockTransactResourceProviderPlugin extends AbstractTransactPlugin { response: ResourceProviderResponse, context: TransactContext ): Promise { - // Establish an AbiProvider based on the session context. - const abiProvider: AbiProvider = { - getAbi: async (account: Name): Promise => { - const response = await context.client.v1.chain.get_abi(account) - if (!response.abi) { - /* istanbul ignore next */ - throw new Error('could not load abi') // TODO: Better coverage for this - } - return response.abi - }, - } - // Create a new signing request based on the response to return to the session's transact flow. const request = await SigningRequest.create( {transaction: response.data.request[1]}, - { - abiProvider, - zlib, - } + context.esrOptions ) // Set the required fee onto the request itself for wallets to process. From 33a335ee28aa891dac64aaa8acbaeac85add361c Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Wed, 11 Jan 2023 10:38:03 -0500 Subject: [PATCH 03/26] Renamed Context `session` to `permissionLevel` --- src/session.ts | 10 +++++----- test/tests/plugins/transact/resource-provider.ts | 4 ++-- test/utils/mock-context.ts | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/session.ts b/src/session.ts index 8e3bd260..c9add043 100644 --- a/src/session.ts +++ b/src/session.ts @@ -79,7 +79,7 @@ export abstract class AbstractWalletPlugin implements WalletPlugin { export interface TransactContextOptions { client: APIClient fetch: Fetch - session: PermissionLevel + permissionLevel: PermissionLevel transactPlugins?: AbstractTransactPlugin[] transactPluginsOptions?: TransactPluginsOptions } @@ -98,13 +98,13 @@ export class TransactContext { afterSign: [], beforeSign: [], } - readonly session: PermissionLevel + readonly permissionLevel: PermissionLevel readonly transactPluginsOptions: TransactPluginsOptions constructor(options: TransactContextOptions) { this.client = options.client this.fetch = options.fetch - this.session = options.session + this.permissionLevel = options.permissionLevel this.transactPluginsOptions = options.transactPluginsOptions || {} options.transactPlugins?.forEach((plugin: AbstractTransactPlugin) => { plugin.register(this) @@ -240,9 +240,9 @@ export class Session { readonly broadcast: boolean = true readonly chain: ChainDefinition readonly fetch: Fetch + readonly permissionLevel: PermissionLevel readonly transactPlugins: TransactPlugin[] readonly transactPluginsOptions: TransactPluginsOptions = {} - readonly permissionLevel: PermissionLevel readonly wallet: WalletPlugin constructor(options: SessionOptions) { @@ -358,9 +358,9 @@ export class Session { const context = new TransactContext({ client: this.client, fetch: this.fetch, + permissionLevel: this.permissionLevel, transactPlugins: options?.transactPlugins || this.transactPlugins, transactPluginsOptions: options?.transactPluginsOptions || this.transactPluginsOptions, - session: this.permissionLevel, }) // Process TransactArgs and convert to a SigningRequest diff --git a/test/tests/plugins/transact/resource-provider.ts b/test/tests/plugins/transact/resource-provider.ts index 117eb1ab..df5b7834 100644 --- a/test/tests/plugins/transact/resource-provider.ts +++ b/test/tests/plugins/transact/resource-provider.ts @@ -76,7 +76,7 @@ export class MockTransactResourceProviderPlugin extends AbstractTransactPlugin { body: JSON.stringify({ ref: 'unittest', request, - signer: context.session, + signer: context.permissionLevel, }), }) @@ -152,7 +152,7 @@ export class MockTransactResourceProviderPlugin extends AbstractTransactPlugin { // Retrieve first authorizer and ensure it matches session context. const firstAction = request.getRawActions()[0] const firstAuthorizer = firstAction.authorization[0] - if (!firstAuthorizer.actor.equals(context.session.actor)) { + if (!firstAuthorizer.actor.equals(context.permissionLevel.actor)) { throw new Error('The first authorizer of the transaction does not match this session.') } } diff --git a/test/utils/mock-context.ts b/test/utils/mock-context.ts index ef23c245..d13b5d21 100644 --- a/test/utils/mock-context.ts +++ b/test/utils/mock-context.ts @@ -10,6 +10,6 @@ export function makeContext(): TransactContext { provider: new FetchProvider(mockUrl, {fetch: mockFetch}), }), fetch: mockFetch, - session: PermissionLevel.from('wharfkit1125@test'), + permissionLevel: PermissionLevel.from('wharfkit1125@test'), }) } From 0e675810d283036f33029f11f2e26a3a667b2c6f Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Wed, 11 Jan 2023 10:42:46 -0500 Subject: [PATCH 04/26] Renamed getters to be more specific Motivation was that I imagine we'll want the `.account` getter to return an Account Kit instance. --- src/session.ts | 4 ++-- test/tests/session.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/session.ts b/src/session.ts index c9add043..cec879e4 100644 --- a/src/session.ts +++ b/src/session.ts @@ -270,11 +270,11 @@ export class Session { this.wallet = options.walletPlugin } - get account(): Name { + get accountName(): Name { return this.permissionLevel.actor } - get permission(): Name { + get permissionName(): Name { return this.permissionLevel.permission } diff --git a/test/tests/session.ts b/test/tests/session.ts index 0bab01f8..5aefb2a3 100644 --- a/test/tests/session.ts +++ b/test/tests/session.ts @@ -191,11 +191,11 @@ suite('session', function () { }) test('getters', function () { assert.equal( - session.account, + session.accountName, PermissionLevel.from(mockSessionOptions.permissionLevel).actor ) assert.equal( - session.permission, + session.permissionName, PermissionLevel.from(mockSessionOptions.permissionLevel).permission ) }) From d5e41b17cbfc364e5299a2da692d758b39e25efa Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Wed, 11 Jan 2023 10:45:14 -0500 Subject: [PATCH 05/26] Added getters to the TransactContext --- src/session.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/session.ts b/src/session.ts index cec879e4..7d5bb80b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -111,6 +111,14 @@ export class TransactContext { }) } + get accountName(): Name { + return this.permissionLevel.actor + } + + get permissionName(): Name { + return this.permissionLevel.permission + } + get abiProvider(): AbiProvider { return { getAbi: async (account: Name): Promise => { From 034d1e162d48dcd6530fc4c309b35138db19d186 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Wed, 11 Jan 2023 11:00:11 -0500 Subject: [PATCH 06/26] Deduplicating code by adding a helper function --- src/session.ts | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/src/session.ts b/src/session.ts index 7d5bb80b..ba00e605 100644 --- a/src/session.ts +++ b/src/session.ts @@ -84,6 +84,20 @@ export interface TransactContextOptions { transactPluginsOptions?: TransactPluginsOptions } +/** + * Given an APIClient instance, this method returns an AbiProvider for use in EOSIO Signing Requests. + */ +export const makeAbiProvider = (client: APIClient): AbiProvider => ({ + getAbi: async (account: Name): Promise => { + const response = await client.v1.chain.get_abi(account) + if (!response.abi) { + /* istanbul ignore next */ + throw new Error('could not load abi') // TODO: Better coverage for this + } + return ABI.from(response.abi) + }, +}) + /** * Temporary context created for the duration of a [[Session.transact]] call. * @@ -120,16 +134,7 @@ export class TransactContext { } get abiProvider(): AbiProvider { - return { - getAbi: async (account: Name): Promise => { - const response = await this.client.v1.chain.get_abi(account) - if (!response.abi) { - /* istanbul ignore next */ - throw new Error('could not load abi') // TODO: Better coverage for this - } - return ABI.from(response.abi) - }, - } + return makeAbiProvider(this.client) } get esrOptions(): SigningRequestEncodingOptions { @@ -318,18 +323,8 @@ export class Session { } async createRequest(args: TransactArgs): Promise { - const abiProvider: AbiProvider = { - getAbi: async (account: Name): Promise => { - const response = await this.client.v1.chain.get_abi(account) - if (!response.abi) { - /* istanbul ignore next */ - throw new Error('could not load abi') // TODO: Better coverage for this - } - return response.abi - }, - } const options = { - abiProvider, + abiProvider: makeAbiProvider(this.client), zlib, } if (args.request && args.request instanceof SigningRequest) { From 624610b222574ad1d9396b5c6be385a0cd15a9a7 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Fri, 13 Jan 2023 13:22:20 -0500 Subject: [PATCH 07/26] Added the ABICache and implemented in the TransactContext --- src/session.ts | 63 ++++++++++++++++++++++++++++++------------- test/tests/context.ts | 12 ++++++--- 2 files changed, 53 insertions(+), 22 deletions(-) diff --git a/src/session.ts b/src/session.ts index ba00e605..54da3151 100644 --- a/src/session.ts +++ b/src/session.ts @@ -3,6 +3,7 @@ import { ABIDef, AnyAction, AnyTransaction, + API, APIClient, Checksum256Type, FetchProvider, @@ -77,6 +78,7 @@ export abstract class AbstractWalletPlugin implements WalletPlugin { * Options for creating a new context for a [[Session.transact]] call. */ export interface TransactContextOptions { + abiCache?: ABICache client: APIClient fetch: Fetch permissionLevel: PermissionLevel @@ -85,18 +87,35 @@ export interface TransactContextOptions { } /** - * Given an APIClient instance, this method returns an AbiProvider for use in EOSIO Signing Requests. + * Given an APIClient instance, this class provides an AbiProvider interface for retrieving and caching ABIs. */ -export const makeAbiProvider = (client: APIClient): AbiProvider => ({ - getAbi: async (account: Name): Promise => { - const response = await client.v1.chain.get_abi(account) - if (!response.abi) { - /* istanbul ignore next */ - throw new Error('could not load abi') // TODO: Better coverage for this +export class ABICache implements AbiProvider { + public readonly cache: Map = new Map() + public readonly pending: Map> = new Map() + + constructor(public readonly client: APIClient) {} + + public async getAbi(account: Name): Promise { + const key = String(account) + let record = this.cache.get(key) + if (!record) { + let getAbi = this.pending.get(key) + if (!getAbi) { + getAbi = this.client.v1.chain.get_abi(account) + this.pending.set(key, getAbi) + } + const response = await getAbi + this.pending.delete(key) + if (response.abi) { + record = ABI.from(response.abi) + this.cache.set(key, record) + } else { + throw new Error(`ABI for ${key} could not be loaded.`) + } } - return ABI.from(response.abi) - }, -}) + return record + } +} /** * Temporary context created for the duration of a [[Session.transact]] call. @@ -105,6 +124,7 @@ export const makeAbiProvider = (client: APIClient): AbiProvider => ({ * provide a way for plugins to add hooks into the process. */ export class TransactContext { + readonly abiCache: ABICache readonly client: APIClient readonly fetch: Fetch readonly hooks: TransactHooks = { @@ -116,6 +136,11 @@ export class TransactContext { readonly transactPluginsOptions: TransactPluginsOptions constructor(options: TransactContextOptions) { + if (options.abiCache) { + this.abiCache = options.abiCache + } else { + this.abiCache = new ABICache(options.client) + } this.client = options.client this.fetch = options.fetch this.permissionLevel = options.permissionLevel @@ -133,13 +158,9 @@ export class TransactContext { return this.permissionLevel.permission } - get abiProvider(): AbiProvider { - return makeAbiProvider(this.client) - } - get esrOptions(): SigningRequestEncodingOptions { return { - abiProvider: this.abiProvider, + abiProvider: new ABICache(this.client), zlib, } } @@ -249,6 +270,7 @@ export interface SessionOptions { } export class Session { + readonly abiCache = ABICache readonly allowModify: boolean = true readonly broadcast: boolean = true readonly chain: ChainDefinition @@ -322,9 +344,9 @@ export class Session { return args } - async createRequest(args: TransactArgs): Promise { + async createRequest(args: TransactArgs, abiCache: ABICache): Promise { const options = { - abiProvider: makeAbiProvider(this.client), + abiProvider: abiCache, zlib, } if (args.request && args.request instanceof SigningRequest) { @@ -357,8 +379,11 @@ export class Session { * F --> H[TransactResult] */ async transact(args: TransactArgs, options?: TransactOptions): Promise { + const abiCache = new ABICache(this.client) + // The context for this transaction const context = new TransactContext({ + abiCache, client: this.client, fetch: this.fetch, permissionLevel: this.permissionLevel, @@ -367,7 +392,7 @@ export class Session { }) // Process TransactArgs and convert to a SigningRequest - const request: SigningRequest = await this.createRequest(args) + const request: SigningRequest = await this.createRequest(args, abiCache) // Create response template to this transact call const result: TransactResult = { @@ -406,7 +431,7 @@ export class Session { const info = await context.client.v1.chain.get_info() const expireSeconds = 120 // TODO: Needs to be configurable by parameters const header = info.getTransactionHeader(expireSeconds) - const abis = await result.request.fetchAbis() // TODO: ABI Cache Implementation + const abis = await result.request.fetchAbis(abiCache) // Resolve the request and get the resolved transaction result.resolved = await result.request.resolve(abis, this.permissionLevel, header) diff --git a/test/tests/context.ts b/test/tests/context.ts index 52581e76..58391255 100644 --- a/test/tests/context.ts +++ b/test/tests/context.ts @@ -10,10 +10,16 @@ const context = makeContext() suite('context', function () { suite('abiProvider', function () { test('has default', function () { - assert.isDefined(context.abiProvider) + assert.isDefined(context.abiCache) }) test('fetches ABIs', async function () { - const result = await context.abiProvider.getAbi(Name.from('eosio.token')) + const result = await context.abiCache.getAbi(Name.from('eosio.token')) + const abi = ABI.from(result) + assert.instanceOf(result, ABI) + assert.equal(abi.version, 'eosio::abi/1.2') + }) + test('caches ABIs', async function () { + const result = await context.abiCache.getAbi(Name.from('eosio.token')) const abi = ABI.from(result) assert.instanceOf(result, ABI) assert.equal(abi.version, 'eosio::abi/1.2') @@ -22,7 +28,7 @@ suite('context', function () { suite('esrOptions', function () { test('has abiProvider', function () { assert.isDefined(context.esrOptions.abiProvider) - assert.hasAllKeys(context.esrOptions.abiProvider, ['getAbi']) + assert.isFunction(context.esrOptions.abiProvider?.getAbi) }) test('has zlib', function () { assert.isDefined(context.esrOptions.zlib) From 9339d332a51f86262d08a0509b388aa2c9f0ff83 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Fri, 13 Jan 2023 13:23:36 -0500 Subject: [PATCH 08/26] Removed unused import --- src/session.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 54da3151..28be97c4 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,6 +1,5 @@ import { ABI, - ABIDef, AnyAction, AnyTransaction, API, From dac6ae619ae628761638b7568ef26fd9d69b50ce Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Sun, 15 Jan 2023 13:24:10 -0500 Subject: [PATCH 09/26] Allow specifying `expireSeconds` throughout the stack Resolves #22 --- src/kit.ts | 7 +++++++ src/session.ts | 14 ++++++++++++- test/tests/kit.ts | 36 ++++++++++++++++++++++++++++++++- test/tests/session.ts | 45 +++++++++++++++++++++++++++++++++++++++++- test/tests/transact.ts | 45 +++++++++++++++++++++++++++++++++++++++++- 5 files changed, 143 insertions(+), 4 deletions(-) diff --git a/src/kit.ts b/src/kit.ts index aaeba9a9..af4226b7 100644 --- a/src/kit.ts +++ b/src/kit.ts @@ -96,6 +96,7 @@ export interface LoginOptions { export interface SessionKitOptions { appName: NameType chains: ChainDefinitionType[] + expireSeconds?: number fetch?: Fetch loginPlugins?: LoginPlugin[] transactPlugins?: TransactPlugin[] @@ -109,6 +110,7 @@ export interface SessionKitOptions { export class SessionKit { readonly appName: Name readonly chains: ChainDefinition[] + readonly expireSeconds: number = 120 readonly fetch?: Fetch readonly loginPlugins: AbstractLoginPlugin[] readonly transactPlugins: AbstractTransactPlugin[] @@ -119,6 +121,10 @@ export class SessionKit { // Store options passed on the kit this.appName = Name.from(options.appName) this.chains = options.chains.map((chain) => ChainDefinition.from(chain)) + // Override default expireSeconds for all sessions if specified + if (options.expireSeconds) { + this.expireSeconds = options.expireSeconds + } // Override fetch if provided if (options.fetch) { this.fetch = options.fetch @@ -168,6 +174,7 @@ export class SessionKit { const chain = this.chains[0] const context: SessionOptions = { chain, + expireSeconds: this.expireSeconds, fetch: this.fetch, permissionLevel: 'eosio@active', transactPlugins: options?.transactPlugins || this.transactPlugins, diff --git a/src/session.ts b/src/session.ts index 28be97c4..f203151e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -204,6 +204,10 @@ export interface TransactOptions { * Chain to use when configured with multiple chains. */ chain?: Checksum256Type + /** + * The number of seconds in the future this transaction will expire. + */ + expireSeconds?: number /** * Specific transact plugins to use for this transaction. */ @@ -261,6 +265,7 @@ export interface SessionOptions { allowModify?: boolean broadcast?: boolean chain: ChainDefinitionType + expireSeconds?: number fetch?: Fetch permissionLevel: PermissionLevelType | string transactPlugins?: AbstractTransactPlugin[] @@ -273,6 +278,7 @@ export class Session { readonly allowModify: boolean = true readonly broadcast: boolean = true readonly chain: ChainDefinition + readonly expireSeconds: number = 120 readonly fetch: Fetch readonly permissionLevel: PermissionLevel readonly transactPlugins: TransactPlugin[] @@ -287,6 +293,9 @@ export class Session { if (options.broadcast !== undefined) { this.broadcast = options.broadcast } + if (options.expireSeconds) { + this.expireSeconds = options.expireSeconds + } if (options.fetch) { this.fetch = options.fetch } else { @@ -409,6 +418,10 @@ export class Session { ? options.allowModify : this.allowModify + // The number of seconds before this transaction expires + const expireSeconds = + options && options.expireSeconds ? options.expireSeconds : this.expireSeconds + // Whether or not the request should be broadcast during the transact call const willBroadcast = options && typeof options.broadcast !== 'undefined' ? options.broadcast : this.broadcast @@ -428,7 +441,6 @@ export class Session { // Resolve SigningRequest with authority + tapos const info = await context.client.v1.chain.get_info() - const expireSeconds = 120 // TODO: Needs to be configurable by parameters const header = info.getTransactionHeader(expireSeconds) const abis = await result.request.fetchAbis(abiCache) diff --git a/test/tests/kit.ts b/test/tests/kit.ts index f8fb1a92..94990fbf 100644 --- a/test/tests/kit.ts +++ b/test/tests/kit.ts @@ -1,13 +1,16 @@ import {assert} from 'chai' import {BaseTransactPlugin, Session, SessionKit, SessionKitOptions} from '$lib' -import {PermissionLevel} from '@greymass/eosio' +import {PermissionLevel, TimePointSec} from '@greymass/eosio' import {makeWallet} from '$test/utils/mock-wallet' import {MockTransactPlugin} from '$test/utils/mock-hook' +import {makeMockAction} from '$test/utils/mock-transfer' import {mockFetch} from '$test/utils/mock-fetch' import {mockPermissionLevel} from '$test/utils/mock-config' +const action = makeMockAction() + const defaultSessionKitOptions: SessionKitOptions = { appName: 'demo.app', chains: [ @@ -27,6 +30,37 @@ suite('kit', function () { assert.instanceOf(sessionKit, SessionKit) }) suite('options', function () { + suite('expireSeconds', function () { + test('default: 120', async function () { + const sessionKit = new SessionKit(defaultSessionKitOptions) + const session = await sessionKit.login() + const result = await session.transact({action}, {broadcast: false}) + // Get the chain info to get the current head block time from test cache + const {head_block_time} = await session.client.v1.chain.get_info() + const expectedExpiration = head_block_time.toMilliseconds() + 120 * 1000 + assert.equal( + String(result.transaction?.expiration), + String(TimePointSec.fromMilliseconds(expectedExpiration)) + ) + }) + test('override: 60', async function () { + const sessionKit = new SessionKit({ + ...defaultSessionKitOptions, + expireSeconds: 60, + }) + const session = await sessionKit.login() + const expireSeconds = 60 + const result = await session.transact({action}, {broadcast: false}) + // Get the chain info to get the current head block time from test cache + const {head_block_time} = await session.client.v1.chain.get_info() + const expectedExpiration = + head_block_time.toMilliseconds() + expireSeconds * 1000 + assert.equal( + String(result.transaction?.expiration), + String(TimePointSec.fromMilliseconds(expectedExpiration)) + ) + }) + }) suite('transactPlugins', function () { test('default', async function () { const sessionKit = new SessionKit(defaultSessionKitOptions) diff --git a/test/tests/session.ts b/test/tests/session.ts index 5aefb2a3..e68dab71 100644 --- a/test/tests/session.ts +++ b/test/tests/session.ts @@ -1,7 +1,7 @@ import {assert} from 'chai' import SessionKit, {BaseTransactPlugin, ChainDefinition, Session, SessionOptions} from '$lib' -import {PermissionLevel} from '@greymass/eosio' +import {PermissionLevel, TimePointSec} from '@greymass/eosio' import {mockFetch} from '$test/utils/mock-fetch' import {MockTransactPlugin, MockTransactResourceProviderPlugin} from '$test/utils/mock-hook' @@ -105,6 +105,49 @@ suite('session', function () { assert.isUndefined(result.response) }) }) + suite('expireSeconds', function () { + test('default: 120', async function () { + const session = new Session({ + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permissionLevel: PermissionLevel.from(mockPermissionLevel), + walletPlugin: wallet, + }) + const result = await session.transact({action}, {broadcast: false}) + // Get the chain info to get the current head block time from test cache + const {head_block_time} = await session.client.v1.chain.get_info() + const expectedExpiration = head_block_time.toMilliseconds() + 120 * 1000 + assert.equal( + String(result.transaction?.expiration), + String(TimePointSec.fromMilliseconds(expectedExpiration)) + ) + }) + test('override: 60', async function () { + const session = new Session({ + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + expireSeconds: 60, + fetch: mockFetch, // Required for unit tests + permissionLevel: PermissionLevel.from(mockPermissionLevel), + walletPlugin: wallet, + }) + const expireSeconds = 60 + const result = await session.transact({action}, {broadcast: false}) + // Get the chain info to get the current head block time from test cache + const {head_block_time} = await session.client.v1.chain.get_info() + const expectedExpiration = + head_block_time.toMilliseconds() + expireSeconds * 1000 + assert.equal( + String(result.transaction?.expiration), + String(TimePointSec.fromMilliseconds(expectedExpiration)) + ) + }) + }) suite('passed as', function () { test('typed values', async function () { const testSession = new Session({ diff --git a/test/tests/transact.ts b/test/tests/transact.ts index 13d3c2b0..5180e6f1 100644 --- a/test/tests/transact.ts +++ b/test/tests/transact.ts @@ -1,7 +1,7 @@ import {assert} from 'chai' import zlib from 'pako' -import {PermissionLevel, Serializer, Signature} from '@greymass/eosio' +import {PermissionLevel, Serializer, Signature, TimePointSec} from '@greymass/eosio' import {ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request' import SessionKit, { @@ -236,6 +236,49 @@ suite('transact', function () { assetValidTransactResponse(result) }) }) + suite('expireSeconds', function () { + test('default: 120', async function () { + const {action} = await mockData() + const session = new Session({ + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permissionLevel: PermissionLevel.from(mockPermissionLevel), + walletPlugin: wallet, + }) + const result = await session.transact({action}, {broadcast: false}) + // Get the chain info to get the current head block time from test cache + const {head_block_time} = await session.client.v1.chain.get_info() + const expectedExpiration = head_block_time.toMilliseconds() + 120 * 1000 + assert.equal( + String(result.transaction?.expiration), + String(TimePointSec.fromMilliseconds(expectedExpiration)) + ) + }) + test('override: 60', async function () { + const {action} = await mockData() + const session = new Session({ + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permissionLevel: PermissionLevel.from(mockPermissionLevel), + walletPlugin: wallet, + }) + const expireSeconds = 60 + const result = await session.transact({action}, {broadcast: false, expireSeconds}) + // Get the chain info to get the current head block time from test cache + const {head_block_time} = await session.client.v1.chain.get_info() + const expectedExpiration = head_block_time.toMilliseconds() + expireSeconds * 1000 + assert.equal( + String(result.transaction?.expiration), + String(TimePointSec.fromMilliseconds(expectedExpiration)) + ) + }) + }) suite('transactPlugins', function () { test('inherit', async function () { const {action} = await mockData() From 828d37a3f366ef5f4b04e63b176e68f42f71611f Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Sun, 15 Jan 2023 13:24:26 -0500 Subject: [PATCH 10/26] An ENV variable to dump http request traffic for debugging --- test/utils/mock-fetch.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/utils/mock-fetch.ts b/test/utils/mock-fetch.ts index 76c4ae0a..bde22c83 100644 --- a/test/utils/mock-fetch.ts +++ b/test/utils/mock-fetch.ts @@ -26,6 +26,9 @@ async function getExisting(filename: string) { } export async function mockFetch(path, params) { + if (process.env['LOGHTTP']) { + console.log('HTTP Request', {path, params}) + } const filename = getFilename(path, params) if (process.env['MOCK'] !== 'overwrite') { const existing = await getExisting(filename) From 823b2b3c62a68576abf098990eead82bcd41fb4b Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Sun, 15 Jan 2023 16:19:41 -0500 Subject: [PATCH 11/26] Add creation of a wharfkitnoop account for cosigning tests --- test/utils/setup/accounts.md | 29 ++++++++--------- test/utils/setup/accounts.ts | 60 ++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 14 deletions(-) diff --git a/test/utils/setup/accounts.md b/test/utils/setup/accounts.md index 6215779f..08f716d3 100644 --- a/test/utils/setup/accounts.md +++ b/test/utils/setup/accounts.md @@ -1,16 +1,17 @@ ## Testing accounts -| account@permission | tokens | cpu | net | ram | -| ------------------ | ------ | --- | --- | --- | -| wharfkit1111@test | ✅ | ✅ | ✅ | ✅ | -| wharfkit1112@test | ✅ | ❌ | ✅ | ✅ | -| wharfkit1113@test | ✅ | ✅ | ❌ | ✅ | -| wharfkit1114@test | ✅ | ✅ | ✅ | ❌ | -| wharfkit1115@test | ✅ | ❌ | ❌ | ❌ | -| wharfkit1121@test | ❌ | ✅ | ✅ | ✅ | -| wharfkit1122@test | ❌ | ❌ | ✅ | ✅ | -| wharfkit1123@test | ❌ | ✅ | ❌ | ✅ | -| wharfkit1124@test | ❌ | ✅ | ✅ | ❌ | -| wharfkit1125@test | ❌ | ❌ | ❌ | ❌ | -| wharfkit1131@test | ✅ | ❌ | ❌ | ✅ | -| wharfkit1132@test | ❌ | ❌ | ❌ | ✅ | +| account@permission | tokens | cpu | net | ram | +| ------------------- | ------ | --- | --- | --- | +| wharfkit1111@test | ✅ | ✅ | ✅ | ✅ | +| wharfkit1112@test | ✅ | ❌ | ✅ | ✅ | +| wharfkit1113@test | ✅ | ✅ | ❌ | ✅ | +| wharfkit1114@test | ✅ | ✅ | ✅ | ❌ | +| wharfkit1115@test | ✅ | ❌ | ❌ | ❌ | +| wharfkit1121@test | ❌ | ✅ | ✅ | ✅ | +| wharfkit1122@test | ❌ | ❌ | ✅ | ✅ | +| wharfkit1123@test | ❌ | ✅ | ❌ | ✅ | +| wharfkit1124@test | ❌ | ✅ | ✅ | ❌ | +| wharfkit1125@test | ❌ | ❌ | ❌ | ❌ | +| wharfkit1131@test | ✅ | ❌ | ❌ | ✅ | +| wharfkit1132@test | ❌ | ❌ | ❌ | ✅ | +| wharfkitnoop@cosign | ✅ | ✅ | ✅ | ✅ | diff --git a/test/utils/setup/accounts.ts b/test/utils/setup/accounts.ts index 75ee0af2..173b9700 100644 --- a/test/utils/setup/accounts.ts +++ b/test/utils/setup/accounts.ts @@ -31,6 +31,9 @@ const controlKey = 'EOS6XXTaRpWhPwnb7CTV9zVsCBrvCpYMMPSk8E8hsJxhf6VFW9DYN' // Test permission key to set on all the accounts const testKey = 'EOS6RMS3nvoN9StPzZizve6WdovaDkE5KkEcCDXW7LbepyAioMiK6' +// Cosigner Key for wharfkitnoop +const noopKey = 'EOS8WUgppBZ1NjnGASYeLwQ3PkNLvdnfnchumsSpo6ApCAzbETczm' + // Minimum RAM bytes to create an account const requiredRamBytes = 1598 @@ -132,6 +135,13 @@ const accounts: AccountDefinition[] = [ netStake: undefined, ramBytes: 10000, }, + { + name: 'wharfkitnoop', + balance: '5.0000 EOS', + cpuStake: '1.0000 EOS', + netStake: '1.0000 EOS', + ramBytes: 10000, + }, ] async function createAccount( @@ -312,6 +322,56 @@ async function createTestPermission(account: AccountDefinition): Promise Date: Sun, 15 Jan 2023 21:19:49 -0500 Subject: [PATCH 12/26] Ignore any local notes --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8e91a2a0..278b6dd3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules/ lib/ -build/ \ No newline at end of file +build/ +notes.md From ed067fbfbdb64bf1a5b71c6c21ad96fe9b00359c Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Sun, 15 Jan 2023 21:20:27 -0500 Subject: [PATCH 13/26] Refector TransactContext and ABICache --- src/abi.ts | 33 +++++++ src/index-module.ts | 2 + src/kit.ts | 10 +- src/session.ts | 232 +++----------------------------------------- src/transact.ts | 196 +++++++++++++++++++++++++++++++++++++ 5 files changed, 247 insertions(+), 226 deletions(-) create mode 100644 src/abi.ts create mode 100644 src/transact.ts diff --git a/src/abi.ts b/src/abi.ts new file mode 100644 index 00000000..37868e0f --- /dev/null +++ b/src/abi.ts @@ -0,0 +1,33 @@ +import {ABI, API, APIClient, Name} from '@greymass/eosio' +import {AbiProvider} from 'eosio-signing-request' + +/** + * Given an APIClient instance, this class provides an AbiProvider interface for retrieving and caching ABIs. + */ +export class ABICache implements AbiProvider { + public readonly cache: Map = new Map() + public readonly pending: Map> = new Map() + + constructor(public readonly client: APIClient) {} + + public async getAbi(account: Name): Promise { + const key = String(account) + let record = this.cache.get(key) + if (!record) { + let getAbi = this.pending.get(key) + if (!getAbi) { + getAbi = this.client.v1.chain.get_abi(account) + this.pending.set(key, getAbi) + } + const response = await getAbi + this.pending.delete(key) + if (response.abi) { + record = ABI.from(response.abi) + this.cache.set(key, record) + } else { + throw new Error(`ABI for ${key} could not be loaded.`) + } + } + return record + } +} diff --git a/src/index-module.ts b/src/index-module.ts index 72b1f9ea..c7989121 100644 --- a/src/index-module.ts +++ b/src/index-module.ts @@ -1,4 +1,6 @@ +export * from './abi' export * from './kit' export * from './plugins' export * from './session' +export * from './transact' export * from './types' diff --git a/src/kit.ts b/src/kit.ts index af4226b7..2e288d43 100644 --- a/src/kit.ts +++ b/src/kit.ts @@ -8,18 +8,14 @@ import { PermissionLevelType, } from '@greymass/eosio' -import {ChainDefinition, ChainDefinitionType, Fetch} from './types' - +import {Session, SessionOptions, WalletPlugin, WalletPluginLoginOptions} from './session' import { AbstractTransactPlugin, BaseTransactPlugin, - Session, - SessionOptions, TransactPlugin, TransactPluginsOptions, - WalletPlugin, - WalletPluginLoginOptions, -} from './session' +} from './transact' +import {ChainDefinition, ChainDefinitionType, Fetch} from './types' export enum LoginHookTypes { beforeLogin = 'beforeLogin', diff --git a/src/session.ts b/src/session.ts index f203151e..d19ec903 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,10 +1,7 @@ import { ABI, - AnyAction, - AnyTransaction, API, APIClient, - Checksum256Type, FetchProvider, Name, PermissionLevel, @@ -12,42 +9,23 @@ import { Signature, SignedTransaction, } from '@greymass/eosio' -import { - AbiProvider, - ResolvedSigningRequest, - ResolvedTransaction, - SigningRequest, - SigningRequestEncodingOptions, -} from 'eosio-signing-request' +import {AbiProvider, ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request' import zlib from 'pako' +import {ABICache} from './abi' +import { + AbstractTransactPlugin, + BaseTransactPlugin, + TransactArgs, + TransactContext, + TransactOptions, + TransactPlugin, + TransactPluginsOptions, + TransactResult, +} from './transact' import {ChainDefinition, ChainDefinitionType, Fetch} from './types' import {getFetch} from './utils' -export type TransactPluginsOptions = Record - -export enum TransactHookTypes { - beforeSign = 'beforeSign', - afterSign = 'afterSign', - afterBroadcast = 'afterBroadcast', -} - -export type TransactHook = ( - request: SigningRequest, - context: TransactContext -) => Promise - -export interface TransactHooks { - afterSign: TransactHook[] - beforeSign: TransactHook[] - afterBroadcast: TransactHook[] -} - -export interface TransactHookResponse { - request: SigningRequest - signatures?: Signature[] -} - export interface WalletPluginOptions { name?: string } @@ -73,191 +51,6 @@ export abstract class AbstractWalletPlugin implements WalletPlugin { public abstract sign(chain: ChainDefinition, transaction: ResolvedSigningRequest): Signature } -/** - * Options for creating a new context for a [[Session.transact]] call. - */ -export interface TransactContextOptions { - abiCache?: ABICache - client: APIClient - fetch: Fetch - permissionLevel: PermissionLevel - transactPlugins?: AbstractTransactPlugin[] - transactPluginsOptions?: TransactPluginsOptions -} - -/** - * Given an APIClient instance, this class provides an AbiProvider interface for retrieving and caching ABIs. - */ -export class ABICache implements AbiProvider { - public readonly cache: Map = new Map() - public readonly pending: Map> = new Map() - - constructor(public readonly client: APIClient) {} - - public async getAbi(account: Name): Promise { - const key = String(account) - let record = this.cache.get(key) - if (!record) { - let getAbi = this.pending.get(key) - if (!getAbi) { - getAbi = this.client.v1.chain.get_abi(account) - this.pending.set(key, getAbi) - } - const response = await getAbi - this.pending.delete(key) - if (response.abi) { - record = ABI.from(response.abi) - this.cache.set(key, record) - } else { - throw new Error(`ABI for ${key} could not be loaded.`) - } - } - return record - } -} - -/** - * Temporary context created for the duration of a [[Session.transact]] call. - * - * This context is used to store the state of the transact request and - * provide a way for plugins to add hooks into the process. - */ -export class TransactContext { - readonly abiCache: ABICache - readonly client: APIClient - readonly fetch: Fetch - readonly hooks: TransactHooks = { - afterBroadcast: [], - afterSign: [], - beforeSign: [], - } - readonly permissionLevel: PermissionLevel - readonly transactPluginsOptions: TransactPluginsOptions - - constructor(options: TransactContextOptions) { - if (options.abiCache) { - this.abiCache = options.abiCache - } else { - this.abiCache = new ABICache(options.client) - } - this.client = options.client - this.fetch = options.fetch - this.permissionLevel = options.permissionLevel - this.transactPluginsOptions = options.transactPluginsOptions || {} - options.transactPlugins?.forEach((plugin: AbstractTransactPlugin) => { - plugin.register(this) - }) - } - - get accountName(): Name { - return this.permissionLevel.actor - } - - get permissionName(): Name { - return this.permissionLevel.permission - } - - get esrOptions(): SigningRequestEncodingOptions { - return { - abiProvider: new ABICache(this.client), - zlib, - } - } - - addHook(t: TransactHookTypes, hook: TransactHook) { - this.hooks[t].push(hook) - } -} - -/** - * Payload accepted by the [[Session.transact]] method. - * Note that one of `action`, `actions` or `transaction` must be set. - */ -export interface TransactArgs { - /** Full transaction to sign. */ - transaction?: AnyTransaction - /** Action to sign. */ - action?: AnyAction - /** Actions to sign. */ - actions?: AnyAction[] - /** An ESR payload */ - request?: SigningRequest | string -} - -/** - * Options for the [[Session.transact]] method. - */ -export interface TransactOptions { - /** - * Whether to allow the signer to make modifications to the request - * (e.g. applying a cosigner action to pay for resources). - * - * Defaults to true if [[broadcast]] is true or unspecified; otherwise false. - */ - allowModify?: boolean - /** - * Whether to broadcast the transaction or just return the signature. - * Defaults to true. - */ - broadcast?: boolean - /** - * Chain to use when configured with multiple chains. - */ - chain?: Checksum256Type - /** - * The number of seconds in the future this transaction will expire. - */ - expireSeconds?: number - /** - * Specific transact plugins to use for this transaction. - */ - transactPlugins?: AbstractTransactPlugin[] - /** - * Optional parameters passed in to the various transact plugins. - */ - transactPluginsOptions?: TransactPluginsOptions -} - -/** - * The response from a [[Session.transact]] call. - */ -export interface TransactResult { - /** The chain that was used. */ - chain: ChainDefinition - /** The SigningRequest representation of the transaction. */ - request: SigningRequest - /** The ResolvedSigningRequest of the transaction */ - resolved: ResolvedSigningRequest | undefined - /** The response from the API after sending the transaction, only present if transaction was broadcast. */ - response?: {[key: string]: any} - /** The transaction signatures. */ - signatures: Signature[] - /** The signer authority. */ - signer: PermissionLevel - /** The resulting transaction. */ - transaction: ResolvedTransaction | undefined -} - -/** - * Interface which a [[Session.transact]] plugin must implement. - */ -export interface TransactPlugin { - register: (context: TransactContext) => void -} - -/** - * Abstract class for [[Session.transact]] plugins to extend. - */ -export abstract class AbstractTransactPlugin implements TransactPlugin { - public abstract register(context: TransactContext): void -} - -export class BaseTransactPlugin extends AbstractTransactPlugin { - register() { - // console.log('Register hooks via context.addHook') - } -} - /** * Options for creating a new instance of a [[Session]]. */ @@ -474,3 +267,4 @@ export class Session { return result } } +export {AbstractTransactPlugin} diff --git a/src/transact.ts b/src/transact.ts new file mode 100644 index 00000000..55a636ec --- /dev/null +++ b/src/transact.ts @@ -0,0 +1,196 @@ +import { + AnyAction, + AnyTransaction, + APIClient, + Checksum256Type, + Name, + PermissionLevel, + Signature, +} from '@greymass/eosio' +import { + ResolvedSigningRequest, + ResolvedTransaction, + SigningRequest, + SigningRequestEncodingOptions, +} from 'eosio-signing-request' +import zlib from 'pako' +import {ABICache} from './abi' + +import {ChainDefinition, Fetch} from './types' + +export type TransactPluginsOptions = Record + +export enum TransactHookTypes { + beforeSign = 'beforeSign', + afterSign = 'afterSign', + afterBroadcast = 'afterBroadcast', +} + +export type TransactHook = ( + request: SigningRequest, + context: TransactContext +) => Promise + +export interface TransactHooks { + afterSign: TransactHook[] + beforeSign: TransactHook[] + afterBroadcast: TransactHook[] +} + +export interface TransactHookResponse { + request: SigningRequest + signatures?: Signature[] +} + +/** + * Options for creating a new context for a [[Session.transact]] call. + */ +export interface TransactContextOptions { + abiCache?: ABICache + client: APIClient + fetch: Fetch + permissionLevel: PermissionLevel + transactPlugins?: AbstractTransactPlugin[] + transactPluginsOptions?: TransactPluginsOptions +} + +/** + * Temporary context created for the duration of a [[Session.transact]] call. + * + * This context is used to store the state of the transact request and + * provide a way for plugins to add hooks into the process. + */ +export class TransactContext { + readonly abiCache: ABICache + readonly client: APIClient + readonly fetch: Fetch + readonly hooks: TransactHooks = { + afterBroadcast: [], + afterSign: [], + beforeSign: [], + } + readonly permissionLevel: PermissionLevel + readonly transactPluginsOptions: TransactPluginsOptions + + constructor(options: TransactContextOptions) { + if (options.abiCache) { + this.abiCache = options.abiCache + } else { + this.abiCache = new ABICache(options.client) + } + this.client = options.client + this.fetch = options.fetch + this.permissionLevel = options.permissionLevel + this.transactPluginsOptions = options.transactPluginsOptions || {} + options.transactPlugins?.forEach((plugin: AbstractTransactPlugin) => { + plugin.register(this) + }) + } + + get accountName(): Name { + return this.permissionLevel.actor + } + + get permissionName(): Name { + return this.permissionLevel.permission + } + + get esrOptions(): SigningRequestEncodingOptions { + return { + abiProvider: new ABICache(this.client), + zlib, + } + } + + addHook(t: TransactHookTypes, hook: TransactHook) { + this.hooks[t].push(hook) + } +} +/** + * Payload accepted by the [[Session.transact]] method. + * Note that one of `action`, `actions` or `transaction` must be set. + */ +export interface TransactArgs { + /** Full transaction to sign. */ + transaction?: AnyTransaction + /** Action to sign. */ + action?: AnyAction + /** Actions to sign. */ + actions?: AnyAction[] + /** An ESR payload */ + request?: SigningRequest | string +} + +/** + * Options for the [[Session.transact]] method. + */ +export interface TransactOptions { + /** + * Whether to allow the signer to make modifications to the request + * (e.g. applying a cosigner action to pay for resources). + * + * Defaults to true if [[broadcast]] is true or unspecified; otherwise false. + */ + allowModify?: boolean + /** + * Whether to broadcast the transaction or just return the signature. + * Defaults to true. + */ + broadcast?: boolean + /** + * Chain to use when configured with multiple chains. + */ + chain?: Checksum256Type + /** + * The number of seconds in the future this transaction will expire. + */ + expireSeconds?: number + /** + * Specific transact plugins to use for this transaction. + */ + transactPlugins?: AbstractTransactPlugin[] + /** + * Optional parameters passed in to the various transact plugins. + */ + transactPluginsOptions?: TransactPluginsOptions +} + +/** + * The response from a [[Session.transact]] call. + */ +export interface TransactResult { + /** The chain that was used. */ + chain: ChainDefinition + /** The SigningRequest representation of the transaction. */ + request: SigningRequest + /** The ResolvedSigningRequest of the transaction */ + resolved: ResolvedSigningRequest | undefined + /** The response from the API after sending the transaction, only present if transaction was broadcast. */ + response?: {[key: string]: any} + /** The transaction signatures. */ + signatures: Signature[] + /** The signer authority. */ + signer: PermissionLevel + /** The resulting transaction. */ + transaction: ResolvedTransaction | undefined +} + +/** + * Interface which a [[Session.transact]] plugin must implement. + */ +export interface TransactPlugin { + register: (context: TransactContext) => void +} + +/** + * Abstract class for [[Session.transact]] plugins to extend. + */ +export abstract class AbstractTransactPlugin implements TransactPlugin { + public abstract register(context: TransactContext): void +} + +export class BaseTransactPlugin extends AbstractTransactPlugin { + register() { + // console.log('Register hooks via context.addHook') + } +} From bcb77a88753db005e88f71a119fc1cd9ff521beb Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Mon, 16 Jan 2023 11:52:54 -0500 Subject: [PATCH 14/26] Allow context to resolve transactions (ease for developers) --- src/session.ts | 20 +++++++------------- src/transact.ts | 13 +++++++++++++ test/tests/context.ts | 16 ++++++++++++++++ test/tests/transact.ts | 33 ++++++++++++++++++++++++++++++++- test/utils/mock-hook.ts | 38 +++++++++++++++++++++++++++++++++----- 5 files changed, 101 insertions(+), 19 deletions(-) diff --git a/src/session.ts b/src/session.ts index d19ec903..d6bbcc6f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -1,6 +1,4 @@ import { - ABI, - API, APIClient, FetchProvider, Name, @@ -9,7 +7,7 @@ import { Signature, SignedTransaction, } from '@greymass/eosio' -import {AbiProvider, ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request' +import {ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request' import zlib from 'pako' import {ABICache} from './abi' import { @@ -193,7 +191,7 @@ export class Session { }) // Process TransactArgs and convert to a SigningRequest - const request: SigningRequest = await this.createRequest(args, abiCache) + let request: SigningRequest = await this.createRequest(args, abiCache) // Create response template to this transact call const result: TransactResult = { @@ -221,10 +219,10 @@ export class Session { // Run the `beforeSign` hooks for (const hook of context.hooks.beforeSign) { - const response = await hook(result.request.clone(), context) + const response = await hook(request.clone(), context) // TODO: Verify we should be cloning the requests here, and write tests to verify they cannot be modified if (allowModify) { - result.request = response.request.clone() + request = response.request.clone() } // If signatures were returned, append them if (response.signatures) { @@ -232,13 +230,9 @@ export class Session { } } - // Resolve SigningRequest with authority + tapos - const info = await context.client.v1.chain.get_info() - const header = info.getTransactionHeader(expireSeconds) - const abis = await result.request.fetchAbis(abiCache) - - // Resolve the request and get the resolved transaction - result.resolved = await result.request.resolve(abis, this.permissionLevel, header) + // Resolve the SigningRequest and assign it to the TransactResult + result.request = request + result.resolved = await context.resolve(request, expireSeconds) result.transaction = result.resolved.resolvedTransaction // Sign transaction based on wallet plugin diff --git a/src/transact.ts b/src/transact.ts index 55a636ec..b96c7fef 100644 --- a/src/transact.ts +++ b/src/transact.ts @@ -105,6 +105,19 @@ export class TransactContext { addHook(t: TransactHookTypes, hook: TransactHook) { this.hooks[t].push(hook) } + + async resolve(request: SigningRequest, expireSeconds = 120): Promise { + // TODO: Cache the info/header first time the context resolves? + // If multiple plugins resolve the same request and call get_info, tapos might change + const info = await this.client.v1.chain.get_info() + const header = info.getTransactionHeader(expireSeconds) + + // Load ABIs required to resolve this request + const abis = await request.fetchAbis(this.abiCache) + + // Resolve the request and return + return request.resolve(abis, this.permissionLevel, header) + } } /** * Payload accepted by the [[Session.transact]] method. diff --git a/test/tests/context.ts b/test/tests/context.ts index 58391255..fa75ec8b 100644 --- a/test/tests/context.ts +++ b/test/tests/context.ts @@ -3,6 +3,9 @@ import {assert} from 'chai' import {ABI, Name} from '@greymass/eosio' import zlib from 'pako' +import {SigningRequest} from '$lib' +import {makeMockAction} from '$test/utils/mock-transfer' + import {makeContext} from '$test/utils/mock-context' const context = makeContext() @@ -35,4 +38,17 @@ suite('context', function () { assert.instanceOf(context.esrOptions.zlib, Object) }) }) + suite('resolve', function () { + test('request', async function () { + const request = await SigningRequest.create( + { + action: makeMockAction(), + chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + }, + {zlib} + ) + const resolved = context.resolve(request) + console.log(resolved) + }) + }) }) diff --git a/test/tests/transact.ts b/test/tests/transact.ts index 5180e6f1..bdcd06fd 100644 --- a/test/tests/transact.ts +++ b/test/tests/transact.ts @@ -14,7 +14,11 @@ import SessionKit, { import {makeClient} from '$test/utils/mock-client' import {mockFetch} from '$test/utils/mock-fetch' -import {MockTransactPlugin, MockTransactResourceProviderPlugin} from '$test/utils/mock-hook' +import { + mockTransactActionPrependerPlugin, + MockTransactPlugin, + MockTransactResourceProviderPlugin, +} from '$test/utils/mock-hook' import {makeMockAction, makeMockActions, makeMockTransaction} from '$test/utils/mock-transfer' import {makeWallet} from '$test/utils/mock-wallet' import {mockPermissionLevel} from '$test/utils/mock-config' @@ -353,6 +357,33 @@ suite('transact', function () { ) assetValidTransactResponse(result) }) + test('multiple unique modifications from plugins', async function () { + const {action, session} = await mockData() + const result = await session.transact( + {action}, + { + transactPlugins: [ + mockTransactActionPrependerPlugin, + mockTransactActionPrependerPlugin, + ], + } + ) + assetValidTransactResponse(result) + if (result && result.transaction && result.transaction.actions) { + assert.lengthOf(result.transaction.actions, 3) + assert.isTrue(result.transaction.actions[0].account.equals('greymassnoop')) + assert.isTrue(result.transaction.actions[1].account.equals('greymassnoop')) + assert.isTrue(result.transaction.actions[2].account.equals('eosio.token')) + // Ensure these two authorizations are random and not the same + assert.isTrue( + !result.transaction.actions[0].authorization[0].actor.equals( + result.transaction.actions[1].authorization[0].actor + ) + ) + } else { + assert.fail('Transaction with actions was not returned in result.') + } + }) }) suite('transactPluginsOptions', function () { test('transact', async function () { diff --git a/test/utils/mock-hook.ts b/test/utils/mock-hook.ts index 97ae7f2c..96bbabf4 100644 --- a/test/utils/mock-hook.ts +++ b/test/utils/mock-hook.ts @@ -31,6 +31,12 @@ export class MockTransactPlugin extends AbstractTransactPlugin { } } +// Needed to load the ABI and work with an `Action` object +class noop extends Struct { + static abiName = 'noop' + static abiFields = [] +} + export async function mockTransactResourceProviderPresignHook( request: SigningRequest, context: TransactContext @@ -47,11 +53,6 @@ export async function mockTransactResourceProviderPresignHook( } // Clone the request for modification const cloned = request.clone() - // Needed to load the ABI and work with an `Action` object - class noop extends Struct { - static abiName = 'noop' - static abiFields = [] - } const newAction = Action.from({ account: 'greymassnoop', name: 'noop', @@ -91,3 +92,30 @@ export class MockTransactResourceProviderPlugin extends AbstractTransactPlugin { context.addHook(TransactHookTypes.beforeSign, mockTransactResourceProviderPresignHook) } } + +export const mockTransactActionPrependerPlugin = { + register: (context) => + context.addHook(TransactHookTypes.beforeSign, async (request, context) => ({ + request: await SigningRequest.create( + { + actions: [ + { + account: 'greymassnoop', + name: 'noop', + authorization: [ + { + actor: [...Array(12)] + .map(() => Math.random().toString(36)[2]) + .join(''), + permission: 'test', + }, + ], + data: {}, + }, + ...request.getRawActions(), + ], + }, + context.esrOptions + ), + })), +} From 6b8e833b70ac0665b8eb651acf3864217454927e Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Mon, 16 Jan 2023 19:04:52 -0500 Subject: [PATCH 15/26] Retain a revision history of request modifications in the TransactResult This will --- src/session.ts | 18 +++++++++++----- src/transact.ts | 55 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/session.ts b/src/session.ts index d6bbcc6f..0930365b 100644 --- a/src/session.ts +++ b/src/session.ts @@ -19,6 +19,7 @@ import { TransactPlugin, TransactPluginsOptions, TransactResult, + TransactRevisions, } from './transact' import {ChainDefinition, ChainDefinitionType, Fetch} from './types' @@ -144,25 +145,26 @@ export class Session { } async createRequest(args: TransactArgs, abiCache: ABICache): Promise { + let request: SigningRequest const options = { abiProvider: abiCache, zlib, } if (args.request && args.request instanceof SigningRequest) { - return SigningRequest.from(String(args.request), options) + request = SigningRequest.from(String(args.request), options) } else if (args.request) { - return SigningRequest.from(args.request, options) + request = SigningRequest.from(args.request, options) } else { args = this.upgradeTransaction(args) - const request = await SigningRequest.create( + request = await SigningRequest.create( { ...args, chainId: this.chain.id, }, options ) - return request } + return request } /** @@ -198,6 +200,7 @@ export class Session { chain: this.chain, request, resolved: undefined, + revisions: new TransactRevisions(request), signatures: [], signer: this.permissionLevel, transaction: undefined, @@ -219,8 +222,13 @@ export class Session { // Run the `beforeSign` hooks for (const hook of context.hooks.beforeSign) { + // Get the response of the hook by passing a clonied request. const response = await hook(request.clone(), context) - // TODO: Verify we should be cloning the requests here, and write tests to verify they cannot be modified + + // Save revision history for developers to debug modifications to requests. + result.revisions.addRevision(response, String(hook), allowModify) + + // If modification is allowed, change the current request. if (allowModify) { request = response.request.clone() } diff --git a/src/transact.ts b/src/transact.ts index b96c7fef..4f6d1f2a 100644 --- a/src/transact.ts +++ b/src/transact.ts @@ -5,6 +5,8 @@ import { Checksum256Type, Name, PermissionLevel, + PublicKey, + Serializer, Signature, } from '@greymass/eosio' import { @@ -166,6 +168,57 @@ export interface TransactOptions { * Optional parameters passed in to the various transact plugins. */ transactPluginsOptions?: TransactPluginsOptions + /** + * Optional parameter to control whether signatures returned from plugins are validated. + */ + validatePluginSignatures?: boolean +} + +export interface TransactRevision { + /** + * Whether or not the context allowed any modification to take effect. + */ + allowModify: boolean + /** + * The string representation of the code executed. + */ + code: string + /** + * If the request was modified by this code. + */ + modified: boolean + /** + * The response from the code that was executed. + */ + response: { + request: string + signatures: string[] + } +} + +export class TransactRevisions { + readonly revisions: TransactRevision[] = [] + constructor(request: SigningRequest) { + this.addRevision({request, signatures: []}, 'original', true) + } + public addRevision(response: TransactHookResponse, code: string, allowModify: boolean) { + // Determine if the new response modifies the request + let modified = false + const previous = this.revisions.at(-1) + if (previous) { + modified = previous.response.request !== String(response.request) + } + // Push this revision in to the stack + this.revisions.push({ + allowModify, + code: String(code), + modified, + response: { + request: String(response.request), + signatures: response.signatures ? Serializer.objectify(response.signatures) : [], + }, + }) + } } /** @@ -180,6 +233,8 @@ export interface TransactResult { resolved: ResolvedSigningRequest | undefined /** The response from the API after sending the transaction, only present if transaction was broadcast. */ response?: {[key: string]: any} + /** An array containing revisions of the transaction as modified by plugins as ESR payloads */ + revisions: TransactRevisions /** The transaction signatures. */ signatures: Signature[] /** The signer authority. */ From d6a2085e3f7106bcdd0af85c40bb77a558144d7b Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Mon, 16 Jan 2023 19:09:29 -0500 Subject: [PATCH 16/26] Removed unused import --- src/transact.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/transact.ts b/src/transact.ts index 4f6d1f2a..89b6c0dd 100644 --- a/src/transact.ts +++ b/src/transact.ts @@ -5,7 +5,6 @@ import { Checksum256Type, Name, PermissionLevel, - PublicKey, Serializer, Signature, } from '@greymass/eosio' From 1b4fe0d5032bb933e0bc61e19031fcdd7b73a9bf Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Mon, 16 Jan 2023 19:13:11 -0500 Subject: [PATCH 17/26] Fix for nodejs v14 Cannot use the at method in older nodejs versions --- src/transact.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/transact.ts b/src/transact.ts index 89b6c0dd..381ae17b 100644 --- a/src/transact.ts +++ b/src/transact.ts @@ -203,7 +203,7 @@ export class TransactRevisions { public addRevision(response: TransactHookResponse, code: string, allowModify: boolean) { // Determine if the new response modifies the request let modified = false - const previous = this.revisions.at(-1) + const previous = this.revisions[this.revisions.length - 1] if (previous) { modified = previous.response.request !== String(response.request) } From dd61225572a217ca8e4b38c6bc447c95141fed8f Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 03:08:34 -0500 Subject: [PATCH 18/26] Manually clone incoming SigningRequest This is to preserve all data and override the zlib/abiProvider values. --- src/session.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/session.ts b/src/session.ts index 0930365b..541be89f 100644 --- a/src/session.ts +++ b/src/session.ts @@ -7,7 +7,13 @@ import { Signature, SignedTransaction, } from '@greymass/eosio' -import {ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request' +import { + RequestDataV2, + RequestDataV3, + RequestSignature, + ResolvedSigningRequest, + SigningRequest, +} from 'eosio-signing-request' import zlib from 'pako' import {ABICache} from './abi' import { @@ -144,6 +150,11 @@ export class Session { return args } + // Lifted from @greymass/eosio-signing-request + private storageType(version: number): typeof RequestDataV3 | typeof RequestDataV2 { + return version === 2 ? RequestDataV2 : RequestDataV3 + } + async createRequest(args: TransactArgs, abiCache: ABICache): Promise { let request: SigningRequest const options = { @@ -151,7 +162,18 @@ export class Session { zlib, } if (args.request && args.request instanceof SigningRequest) { - request = SigningRequest.from(String(args.request), options) + // Lifted from @greymass/eosio-signing-request method `clone()` + // This was done to modify the zlib and abiProvider + // TODO: Modify ESR library to expose this `clone()` functionality + let signature: RequestSignature | undefined + if (args.request.signature) { + signature = RequestSignature.from( + JSON.parse(JSON.stringify(args.request.signature)) + ) + } + const RequestData = this.storageType(args.request.version) + const data = RequestData.from(JSON.parse(JSON.stringify(args.request.data))) + request = new SigningRequest(args.request.version, data, zlib, abiCache, signature) } else if (args.request) { request = SigningRequest.from(args.request, options) } else { From 373181ddbec2781892409c3c78a71c8da7a98f1a Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 11:40:42 -0500 Subject: [PATCH 19/26] Migrate plugin tests into new suite --- test/tests/transact.ts | 72 ++++++++++++++++++++++-------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/test/tests/transact.ts b/test/tests/transact.ts index bdcd06fd..063703a3 100644 --- a/test/tests/transact.ts +++ b/test/tests/transact.ts @@ -349,41 +349,6 @@ suite('transact', function () { assert.fail('Transaction with actions was not returned in result.') } }) - test('triggers', async function () { - const {action, session} = await mockData() - const result = await session.transact( - {action}, - {transactPlugins: [new MockTransactPlugin()]} - ) - assetValidTransactResponse(result) - }) - test('multiple unique modifications from plugins', async function () { - const {action, session} = await mockData() - const result = await session.transact( - {action}, - { - transactPlugins: [ - mockTransactActionPrependerPlugin, - mockTransactActionPrependerPlugin, - ], - } - ) - assetValidTransactResponse(result) - if (result && result.transaction && result.transaction.actions) { - assert.lengthOf(result.transaction.actions, 3) - assert.isTrue(result.transaction.actions[0].account.equals('greymassnoop')) - assert.isTrue(result.transaction.actions[1].account.equals('greymassnoop')) - assert.isTrue(result.transaction.actions[2].account.equals('eosio.token')) - // Ensure these two authorizations are random and not the same - assert.isTrue( - !result.transaction.actions[0].authorization[0].actor.equals( - result.transaction.actions[1].authorization[0].actor - ) - ) - } else { - assert.fail('Transaction with actions was not returned in result.') - } - }) }) suite('transactPluginsOptions', function () { test('transact', async function () { @@ -479,6 +444,43 @@ suite('transact', function () { }) }) }) + suite('plugins', function () { + test('trigger', async function () { + const {action, session} = await mockData() + const result = await session.transact( + {action}, + {transactPlugins: [new MockTransactPlugin()]} + ) + assetValidTransactResponse(result) + }) + test('multiple modifications', async function () { + const {action, session} = await mockData() + const result = await session.transact( + {action}, + { + transactPlugins: [ + mockTransactActionPrependerPlugin, + mockTransactActionPrependerPlugin, + ], + } + ) + assetValidTransactResponse(result) + if (result && result.transaction && result.transaction.actions) { + assert.lengthOf(result.transaction.actions, 3) + assert.isTrue(result.transaction.actions[0].account.equals('greymassnoop')) + assert.isTrue(result.transaction.actions[1].account.equals('greymassnoop')) + assert.isTrue(result.transaction.actions[2].account.equals('eosio.token')) + // Ensure these two authorizations are random and not the same + assert.isTrue( + !result.transaction.actions[0].authorization[0].actor.equals( + result.transaction.actions[1].authorization[0].actor + ) + ) + } else { + assert.fail('Transaction with actions was not returned in result.') + } + }) + }) suite('response', function () { test('type check', async function () { const {session, transaction} = await mockData() From 7d1e7f62c5bb03cea7afbab0cfb836f27f486e5a Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 12:16:41 -0500 Subject: [PATCH 20/26] Split cloneRequest into its own function --- src/session.ts | 62 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 18 deletions(-) diff --git a/src/session.ts b/src/session.ts index 541be89f..c747291e 100644 --- a/src/session.ts +++ b/src/session.ts @@ -8,6 +8,7 @@ import { SignedTransaction, } from '@greymass/eosio' import { + AbiProvider, RequestDataV2, RequestDataV3, RequestSignature, @@ -150,30 +151,52 @@ export class Session { return args } - // Lifted from @greymass/eosio-signing-request + /** + * Lifted from @greymass/eosio-signing-request. + * + * TODO: Remove. This will no longer be needed once the `clone` functionality in ESR is updated + */ private storageType(version: number): typeof RequestDataV3 | typeof RequestDataV2 { return version === 2 ? RequestDataV2 : RequestDataV3 } - async createRequest(args: TransactArgs, abiCache: ABICache): Promise { + /** + * Create a clone of the given SigningRequest + * + * @param {SigningRequest} request + * @param {AbiProvider} abiProvider + * @returns Returns a cloned SigningRequest with updated abiProvider and zlib + */ + cloneRequest(request: SigningRequest, abiProvider: AbiProvider): SigningRequest { + // Lifted from @greymass/eosio-signing-request method `clone()` + // This was done to modify the zlib and abiProvider + // TODO: Modify ESR library to expose this `clone()` functionality + // TODO: This if statement should potentially just be: + // request = args.request.clone(abiProvider, zlib) + let signature: RequestSignature | undefined + if (request.signature) { + signature = RequestSignature.from(JSON.parse(JSON.stringify(request.signature))) + } + const RequestData = this.storageType(request.version) + const data = RequestData.from(JSON.parse(JSON.stringify(request.data))) + return new SigningRequest(request.version, data, zlib, abiProvider, signature) + } + + /** + * Convert any provided form of TransactArgs to a SigningRequest + * + * @param {TransactArgs} args + * @param {AbiProvider} abiProvider + * @returns Returns a SigningRequest + */ + async createRequest(args: TransactArgs, abiProvider: AbiProvider): Promise { let request: SigningRequest const options = { - abiProvider: abiCache, + abiProvider, zlib, } if (args.request && args.request instanceof SigningRequest) { - // Lifted from @greymass/eosio-signing-request method `clone()` - // This was done to modify the zlib and abiProvider - // TODO: Modify ESR library to expose this `clone()` functionality - let signature: RequestSignature | undefined - if (args.request.signature) { - signature = RequestSignature.from( - JSON.parse(JSON.stringify(args.request.signature)) - ) - } - const RequestData = this.storageType(args.request.version) - const data = RequestData.from(JSON.parse(JSON.stringify(args.request.data))) - request = new SigningRequest(args.request.version, data, zlib, abiCache, signature) + request = this.cloneRequest(args.request, abiProvider) } else if (args.request) { request = SigningRequest.from(args.request, options) } else { @@ -192,14 +215,17 @@ export class Session { /** * Perform a transaction using this session. * + * @param {TransactArgs} args + * @param {TransactOptions} options + * @returns {TransactResult} The status and data gathered during the operation. * @mermaid - Transaction sequence diagram * flowchart LR * A((Transact)) --> B{{"Hook(s): beforeSign"}} * B --> C[Wallet Plugin] * C --> D{{"Hook(s): afterSign"}} - * D --> F[Broadcast Plugin] - * E --> G{{"Hook(s): afterBroadcast"}} - * F --> H[TransactResult] + * D --> E[Broadcast Plugin] + * E --> F{{"Hook(s): afterBroadcast"}} + * F --> G[TransactResult] */ async transact(args: TransactArgs, options?: TransactOptions): Promise { const abiCache = new ABICache(this.client) From 15cf193661ebe522e6ee7dec80ae5348c6abf6e6 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 12:30:16 -0500 Subject: [PATCH 21/26] Added and implemented updateRequest method Resolves #18. This new update method ensures the metadata from the original request will persist through modification by a `beforeSign` plugin. The original metadata will take precedent over metadata from the new request, ensuring no plugin upstream can overwrite it. A warning will be displayed when this happens to let the developer know. --- src/session.ts | 32 +++++++++++++++++++- test/tests/transact.ts | 65 +++++++++++++++++++++++++++++++++++++++++ test/utils/mock-hook.ts | 10 +++++++ 3 files changed, 106 insertions(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index c747291e..3c23f86a 100644 --- a/src/session.ts +++ b/src/session.ts @@ -212,6 +212,36 @@ export class Session { return request } + /** + * Update a SigningRequest, ensuring its old metadata is retained. + * + * @param {SigningRequest} previous + * @param {SigningRequest} modified + * @param abiProvider + * @returns + */ + async updateRequest( + previous: SigningRequest, + modified: SigningRequest, + abiProvider: AbiProvider + ): Promise { + const updatedRequest: SigningRequest = this.cloneRequest(modified, abiProvider) + const info = updatedRequest.getRawInfo() + // Take all the metadata from the previous and set it on the modified request. + // This will preserve the metadata as it is modified by various plugins. + previous.data.info.forEach((metadata) => { + if (info[metadata.key]) { + // eslint-disable-next-line no-console -- warn the developer since this may be unintentional + console.warn( + `During an updateRequest call, the previous request had already set the ` + + `metadata key of "${metadata.key}" which will not be overwritten.` + + ) + } + updatedRequest.setRawInfoKey(metadata.key, metadata.value) + }) + return updatedRequest + } + /** * Perform a transaction using this session. * @@ -278,7 +308,7 @@ export class Session { // If modification is allowed, change the current request. if (allowModify) { - request = response.request.clone() + request = await this.updateRequest(request, response.request, abiCache) } // If signatures were returned, append them if (response.signatures) { diff --git a/test/tests/transact.ts b/test/tests/transact.ts index 063703a3..e711bbf0 100644 --- a/test/tests/transact.ts +++ b/test/tests/transact.ts @@ -5,6 +5,7 @@ import {PermissionLevel, Serializer, Signature, TimePointSec} from '@greymass/eo import {ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request' import SessionKit, { + ABICache, ChainDefinition, Session, SessionOptions, @@ -15,6 +16,7 @@ import SessionKit, { import {makeClient} from '$test/utils/mock-client' import {mockFetch} from '$test/utils/mock-fetch' import { + mockMetadataFooWriterPlugin, mockTransactActionPrependerPlugin, MockTransactPlugin, MockTransactResourceProviderPlugin, @@ -138,6 +140,20 @@ suite('transact', function () { }) assetValidTransactResponse(result) }) + test('string maintains payload metadata', async function () { + const {session} = await mockData() + const result = await session.transact( + { + request: + 'esr://gmNgZGBY1mTC_MoglIGBIVzX5uxZRgEnjpsHS30fM4DAhI2nLGACDRsnxsWq9Z6yZAVLMbC4-geDaPHyjMSitOzMEoXMYoWSjFSFpNTiEgUbY0YGRua0_HzmpMQiAA', + }, + { + broadcast: false, + transactPlugins: [], + } + ) + assert.equal(result.request.getInfoKey('foo'), 'bar') + }) test('object', async function () { const {session} = await mockData() const result = await session.transact({ @@ -148,6 +164,27 @@ suite('transact', function () { }) assetValidTransactResponse(result) }) + test('object maintains payload metadata', async function () { + const {action, session} = await mockData() + const abiCache = new ABICache(this.client) + const request = await SigningRequest.create( + {action}, + { + abiProvider: abiCache, + zlib, + } + ) + request.setInfoKey('foo', 'bar') + assert.equal(request.getInfoKey('foo'), 'bar') + const result = await session.transact( + {request}, + { + broadcast: false, + transactPlugins: [], + } + ) + assert.equal(result.request.getInfoKey('foo'), 'bar') + }) }) suite('invalid', function () { test('no abi for contract', async function () { @@ -480,6 +517,34 @@ suite('transact', function () { assert.fail('Transaction with actions was not returned in result.') } }) + test('metadata persists through mutation', async function () { + const {session} = await mockData() + const result = await session.transact( + { + request: + 'esr://gmNgZGBY1mTC_MoglIGBIVzX5uxZRgEnjpsHS30fM4DAhI2nLGACDRsnxsWq9Z6yZAVLMbC4-geDaPHyjMSitOzMEoXMYoWSjFSFpNTiEgUbY0YGRua0_HzmpMQiAA', + }, + { + broadcast: false, + transactPlugins: [mockTransactActionPrependerPlugin], + } + ) + assert.equal(result.request.getInfoKey('foo'), 'bar') + }) + test('metadata preservation from original', async function () { + const {session} = await mockData() + const result = await session.transact( + { + request: + 'esr://gmNgZGBY1mTC_MoglIGBIVzX5uxZRgEnjpsHS30fM4DAhI2nLGACDRsnxsWq9Z6yZAVLMbC4-geDaPHyjMSitOzMEoXMYoWSjFSFpNTiEgUbY0YGRua0_HzmpMQiAA', + }, + { + broadcast: false, + transactPlugins: [mockMetadataFooWriterPlugin], + } + ) + assert.equal(result.request.getInfoKey('foo'), 'bar') + }) }) suite('response', function () { test('type check', async function () { diff --git a/test/utils/mock-hook.ts b/test/utils/mock-hook.ts index 96bbabf4..6b1bc6ce 100644 --- a/test/utils/mock-hook.ts +++ b/test/utils/mock-hook.ts @@ -119,3 +119,13 @@ export const mockTransactActionPrependerPlugin = { ), })), } + +export const mockMetadataFooWriterPlugin = { + register: (context) => + context.addHook(TransactHookTypes.beforeSign, async (request) => { + request.setInfoKey('foo', 'baz') + return { + request, + } + }), +} From 9a1c437983633a38627624783930b45539cfc866 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 12:34:34 -0500 Subject: [PATCH 22/26] Syntax --- src/session.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/session.ts b/src/session.ts index 3c23f86a..95a623e6 100644 --- a/src/session.ts +++ b/src/session.ts @@ -234,7 +234,7 @@ export class Session { // eslint-disable-next-line no-console -- warn the developer since this may be unintentional console.warn( `During an updateRequest call, the previous request had already set the ` + - `metadata key of "${metadata.key}" which will not be overwritten.` + + `metadata key of "${metadata.key}" which will not be overwritten.` ) } updatedRequest.setRawInfoKey(metadata.key, metadata.value) From 8185dfb72e7df03610692059382d4778c903fb2c Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Mon, 16 Jan 2023 21:54:18 -0500 Subject: [PATCH 23/26] Adding an appendAction and prependAction helper --- src/utils.ts | 39 +++++++++- test/tests/utils.ts | 150 ++++++++++++++++++++++++++++++++++++ test/utils/mock-data.ts | 22 ++++++ test/utils/mock-hook.ts | 22 +----- test/utils/mock-session.ts | 20 +++++ test/utils/mock-transfer.ts | 1 + 6 files changed, 234 insertions(+), 20 deletions(-) create mode 100644 test/tests/utils.ts create mode 100644 test/utils/mock-data.ts create mode 100644 test/utils/mock-session.ts diff --git a/src/utils.ts b/src/utils.ts index 1776c30d..c318ae92 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,5 @@ -import {FetchProviderOptions} from '@greymass/eosio' +import {Action, AnyAction, FetchProviderOptions, Transaction} from '@greymass/eosio' +import {SigningRequest} from 'eosio-signing-request' import {Fetch} from './types' export function getFetch(options?: FetchProviderOptions): Fetch { @@ -13,3 +14,39 @@ export function getFetch(options?: FetchProviderOptions): Fetch { } throw new Error('Missing fetch') } + +export function appendAction(request: SigningRequest, action: AnyAction): SigningRequest { + const newAction = Action.from(action) + const cloned = request.clone() + if (cloned.data.req.value instanceof Action) { + // Overwrite the data + cloned.data.req.value = [cloned.data.req.value, newAction] + // This needs to be done to indicate it's an `Action[]` + cloned.data.req.variantIdx = 1 + } else if (cloned.data.req.value instanceof Array) { + // Prepend the action to the existing array + cloned.data.req.value.push(newAction) + } else if (cloned.data.req.value instanceof Transaction) { + // Prepend the action to the existing array of the transaction + cloned.data.req.value.actions.push(newAction) + } + return cloned +} + +export function prependAction(request: SigningRequest, action: AnyAction): SigningRequest { + const newAction = Action.from(action) + const cloned = request.clone() + if (cloned.data.req.value instanceof Action) { + // Overwrite the data + cloned.data.req.value = [newAction, cloned.data.req.value] + // This needs to be done to indicate it's an `Action[]` + cloned.data.req.variantIdx = 1 + } else if (cloned.data.req.value instanceof Array) { + // Prepend the action to the existing array + cloned.data.req.value.unshift(newAction) + } else if (cloned.data.req.value instanceof Transaction) { + // Prepend the action to the existing array of the transaction + cloned.data.req.value.actions.unshift(newAction) + } + return cloned +} diff --git a/test/tests/utils.ts b/test/tests/utils.ts new file mode 100644 index 00000000..a5eb2df2 --- /dev/null +++ b/test/tests/utils.ts @@ -0,0 +1,150 @@ +import {assert} from 'chai' + +import zlib from 'pako' + +import {SigningRequest, Transaction} from '$lib' +import {makeMockAction} from '$test/utils/mock-transfer' + +import {appendAction, prependAction} from 'src/utils' +import {mockData} from '$test/utils/mock-data' + +const newAction = makeMockAction('new action') + +function commonAsserts( + original: Transaction, + modified: Transaction, + oldActions = 1, + newActions = 2 +) { + // Ensure no data besides the actions has changed + original.context_free_actions.forEach((action, index) => { + assert.isTrue(action.equals(modified.context_free_actions[index])) + }) + modified.context_free_actions.forEach((action, index) => { + assert.isTrue(action.equals(original.context_free_actions[index])) + }) + assert.isTrue(original.delay_sec.equals(modified.delay_sec)) + assert.isTrue(original.expiration.equals(modified.expiration)) + assert.isTrue(!original.id.equals(modified.id)) + assert.isTrue(original.max_cpu_usage_ms.equals(modified.max_cpu_usage_ms)) + assert.isTrue(original.max_net_usage_words.equals(modified.max_net_usage_words)) + assert.isTrue(original.ref_block_num.equals(modified.ref_block_num)) + assert.isTrue(original.ref_block_prefix.equals(modified.ref_block_prefix)) + original.transaction_extensions.forEach((extension, index) => { + assert.isTrue(extension.equals(modified.transaction_extensions[index])) + }) + modified.transaction_extensions.forEach((extension, index) => { + assert.isTrue(extension.equals(original.transaction_extensions[index])) + }) + + // Ensure the original transaction remains + assert.equal(original.actions.length, oldActions) + + // Ensure the modified transaction updated correctly + assert.equal(modified.actions.length, newActions) +} + +suite('utils', function () { + suite('appendAction', function () { + test('payload w/ action', async function () { + const {action} = await mockData('old action') + const request = await SigningRequest.create( + { + action, + chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + }, + {zlib} + ) + const originalTransaction = request.getRawTransaction() + const modifiedRequest = appendAction(request, newAction) + const modifiedTransaction = modifiedRequest.getRawTransaction() + commonAsserts(originalTransaction, modifiedTransaction) + assert.isTrue(originalTransaction.actions[0].equals(modifiedTransaction.actions[0])) + assert.isTrue(newAction.equals(modifiedTransaction.actions[1])) + }) + test('payload w/ actions', async function () { + const {action} = await mockData('old action') + const request = await SigningRequest.create( + { + actions: [action, action], + chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + }, + {zlib} + ) + const originalTransaction = request.getRawTransaction() + const modifiedRequest = appendAction(request, newAction) + const modifiedTransaction = modifiedRequest.getRawTransaction() + commonAsserts(originalTransaction, modifiedTransaction, 2, 3) + assert.isTrue(originalTransaction.actions[0].equals(modifiedTransaction.actions[0])) + assert.isTrue(originalTransaction.actions[1].equals(modifiedTransaction.actions[1])) + assert.isTrue(newAction.equals(modifiedTransaction.actions[2])) + }) + test('payload w/ transaction', async function () { + const {transaction} = await mockData('old action') + const request = await SigningRequest.create( + { + transaction, + chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + }, + {zlib} + ) + const originalTransaction = request.getRawTransaction() + const modifiedRequest = appendAction(request, newAction) + const modifiedTransaction = modifiedRequest.getRawTransaction() + commonAsserts(originalTransaction, modifiedTransaction) + assert.isTrue(originalTransaction.actions[0].equals(modifiedTransaction.actions[0])) + assert.isTrue(newAction.equals(modifiedTransaction.actions[1])) + }) + }) + suite('prependAction', function () { + test('payload w/ action', async function () { + const {action} = await mockData('old action') + const request = await SigningRequest.create( + { + action, + chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + }, + {zlib} + ) + const originalTransaction = request.getRawTransaction() + const modifiedRequest = prependAction(request, newAction) + const modifiedTransaction = modifiedRequest.getRawTransaction() + commonAsserts(originalTransaction, modifiedTransaction) + assert.isTrue(newAction.equals(modifiedTransaction.actions[0])) + assert.isTrue(originalTransaction.actions[0].equals(modifiedTransaction.actions[1])) + }) + test('payload w/ actions', async function () { + const {action} = await mockData('old action') + const request = await SigningRequest.create( + { + actions: [action, action], + chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + }, + {zlib} + ) + const originalTransaction = request.getRawTransaction() + const modifiedRequest = prependAction(request, newAction) + const modifiedTransaction = modifiedRequest.getRawTransaction() + commonAsserts(originalTransaction, modifiedTransaction, 2, 3) + assert.isTrue(newAction.equals(modifiedTransaction.actions[0])) + assert.isTrue(originalTransaction.actions[0].equals(modifiedTransaction.actions[1])) + assert.isTrue(originalTransaction.actions[1].equals(modifiedTransaction.actions[2])) + }) + test('payload w/ transaction', async function () { + const {transaction} = await mockData('old action') + const request = await SigningRequest.create( + { + transaction, + chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + }, + {zlib} + ) + const originalTransaction = request.getRawTransaction() + const modifiedRequest = prependAction(request, newAction) + const modifiedTransaction = modifiedRequest.getRawTransaction() + commonAsserts(originalTransaction, modifiedTransaction) + assert.isTrue(newAction.equals(modifiedTransaction.actions[0])) + assert.isTrue(originalTransaction.actions[0].equals(modifiedTransaction.actions[1])) + }) + }) +}) diff --git a/test/utils/mock-data.ts b/test/utils/mock-data.ts new file mode 100644 index 00000000..4ba81b75 --- /dev/null +++ b/test/utils/mock-data.ts @@ -0,0 +1,22 @@ +import {Session} from '$lib' + +import {makeClient} from '$test/utils/mock-client' +import {mockSessionOptions} from './mock-session' +import {makeMockAction, makeMockActions, makeMockTransaction} from '$test/utils/mock-transfer' + +const client = makeClient() + +export async function mockData(memo?: string) { + const info = await client.v1.chain.get_info() + const action = await makeMockAction(memo) + const actions = await makeMockActions(memo) + const transaction = await makeMockTransaction(info, memo) + const session = new Session(mockSessionOptions) + return { + action, + actions, + info, + session, + transaction, + } +} diff --git a/test/utils/mock-hook.ts b/test/utils/mock-hook.ts index 6b1bc6ce..4830278a 100644 --- a/test/utils/mock-hook.ts +++ b/test/utils/mock-hook.ts @@ -10,6 +10,7 @@ import { TransactHookTypes, Transaction, } from '$lib' +import {prependAction} from 'src/utils' export async function mockLoginHook(context: SessionOptions) { // Mock hook that does nothing @@ -51,8 +52,6 @@ export async function mockTransactResourceProviderPresignHook( signatures: [], } } - // Clone the request for modification - const cloned = request.clone() const newAction = Action.from({ account: 'greymassnoop', name: 'noop', @@ -64,25 +63,10 @@ export async function mockTransactResourceProviderPresignHook( ], data: noop.from({}), }) - // TODO: Couldn't work with normal objects here - // Needs to do a bunch of conditional logic - shoulnd't be required for a hook - if (cloned.data.req.value instanceof Action) { - // Overwrite the data - cloned.data.req.value = [newAction, cloned.data.req.value] - // This needs to be done to indicate it's an `Action[]` - cloned.data.req.variantIdx = 1 - } else if (cloned.data.req.value instanceof Array) { - // Prepend the action to the existing array - cloned.data.req.value.unshift(newAction) - } else if (cloned.data.req.value instanceof Transaction) { - // Prepend the action to the existing array of the transaction - cloned.data.req.value.actions.unshift(newAction) - } else { - throw new Error('Unrecognized data type in request.') - } + const modified = prependAction(request, newAction) // Return the request return { - request: cloned, + request: modified, signatures: [], } } diff --git a/test/utils/mock-session.ts b/test/utils/mock-session.ts new file mode 100644 index 00000000..925307ba --- /dev/null +++ b/test/utils/mock-session.ts @@ -0,0 +1,20 @@ +import {PermissionLevel} from '@greymass/eosio' + +import {ChainDefinition, SessionOptions} from '$lib' + +import {mockPermissionLevel} from '$test/utils/mock-config' +import {mockFetch} from '$test/utils/mock-fetch' +import {makeWallet} from '$test/utils/mock-wallet' + +const wallet = makeWallet() + +export const mockSessionOptions: SessionOptions = { + broadcast: false, // Disable broadcasting by default for tests, enable when required. + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permissionLevel: PermissionLevel.from(mockPermissionLevel), + walletPlugin: wallet, +} diff --git a/test/utils/mock-transfer.ts b/test/utils/mock-transfer.ts index 4e690a63..9bf6cd18 100644 --- a/test/utils/mock-transfer.ts +++ b/test/utils/mock-transfer.ts @@ -1,4 +1,5 @@ import {Action, API, Asset, Name, Struct, Transaction} from '@greymass/eosio' + import {mockAccountName, mockPermissionName} from './mock-config' @Struct.type('transfer') From 4b8b5efa2a206d8cfc986beefd19342523b46271 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 12:55:44 -0500 Subject: [PATCH 24/26] Finished testing resolve --- test/tests/context.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/test/tests/context.ts b/test/tests/context.ts index fa75ec8b..d9a6076c 100644 --- a/test/tests/context.ts +++ b/test/tests/context.ts @@ -1,6 +1,6 @@ import {assert} from 'chai' -import {ABI, Name} from '@greymass/eosio' +import {ABI, Checksum256, Name, PermissionLevel, Transaction} from '@greymass/eosio' import zlib from 'pako' import {SigningRequest} from '$lib' @@ -47,8 +47,17 @@ suite('context', function () { }, {zlib} ) - const resolved = context.resolve(request) - console.log(resolved) + const resolved = await context.resolve(request) + assert.isTrue( + resolved.chainId.equals( + Checksum256.from( + '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d' + ) + ) + ) + assert.instanceOf(resolved.request, SigningRequest) + assert.instanceOf(resolved.signer, PermissionLevel) + assert.instanceOf(resolved.transaction, Transaction) }) }) }) From c059c4c8ded8a6904a68fc6da2e46c1c071448e1 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 14:32:09 -0500 Subject: [PATCH 25/26] Fixed browser test runner --- test/rollup.config.js | 5 ++++- test/utils/browser-fetch.ts | 26 ++++++++++++++++++++++++++ test/utils/browser-provider.ts | 27 --------------------------- test/utils/mock-client.ts | 2 +- test/utils/mock-context.ts | 2 +- test/utils/setup/accounts.ts | 2 +- 6 files changed, 33 insertions(+), 31 deletions(-) create mode 100644 test/utils/browser-fetch.ts delete mode 100644 test/utils/browser-provider.ts diff --git a/test/rollup.config.js b/test/rollup.config.js index eb0791c1..2c85b73b 100644 --- a/test/rollup.config.js +++ b/test/rollup.config.js @@ -90,7 +90,10 @@ export default [ alias({ entries: [ {find: '$lib', replacement: path.join(__dirname, '..', 'lib/session.m.js')}, - {find: './utils/mock-provider', replacement: './utils/browser-provider.ts'}, + { + find: '$test/utils/mock-fetch', + replacement: './test/utils/browser-fetch.ts', + }, ], }), typescript({target: 'es6', module: 'esnext', tsconfig: './test/tsconfig.json'}), diff --git a/test/utils/browser-fetch.ts b/test/utils/browser-fetch.ts new file mode 100644 index 00000000..76fa9ecf --- /dev/null +++ b/test/utils/browser-fetch.ts @@ -0,0 +1,26 @@ +import {Bytes, Checksum160} from '@greymass/eosio' + +const data = global.MOCK_DATA + +export function getFilename(path, params) { + const digest = Checksum160.hash( + Bytes.from(path + (params ? JSON.stringify(params) : ''), 'utf8') + ).hexString + return digest + '.json' +} + +async function getExisting(filename) { + return data[filename] +} + +export async function mockFetch(path, params) { + const filename = getFilename(path, params) + const existing = await getExisting(filename) + if (existing) { + return new Response(existing.text, { + status: existing.status, + headers: existing.headers, + }) + } + throw new Error(`No data for ${path}`) +} diff --git a/test/utils/browser-provider.ts b/test/utils/browser-provider.ts deleted file mode 100644 index 33a910ae..00000000 --- a/test/utils/browser-provider.ts +++ /dev/null @@ -1,27 +0,0 @@ -import {APIProvider, Bytes, Checksum160} from '@greymass/eosio' - -const data = global.MOCK_DATA - -export class MockProvider implements APIProvider { - constructor(private api: string = 'https://jungle4.greymass.com') {} - - getFilename(path: string, params?: unknown) { - const digest = Checksum160.hash( - Bytes.from(this.api + path + (params ? JSON.stringify(params) : ''), 'utf8') - ).hexString - return digest + '.json' - } - - async getExisting(filename: string) { - return data[filename] - } - - async call(path: string, params?: unknown) { - const filename = this.getFilename(path, params) - const existing = await this.getExisting(filename) - if (existing) { - return existing - } - throw new Error(`No data for ${path}`) - } -} diff --git a/test/utils/mock-client.ts b/test/utils/mock-client.ts index 2c2d8a72..fa10c144 100644 --- a/test/utils/mock-client.ts +++ b/test/utils/mock-client.ts @@ -1,7 +1,7 @@ import {APIClient, FetchProvider} from '@greymass/eosio' import {mockUrl} from './mock-config' -import {mockFetch} from './mock-fetch' +import {mockFetch} from '$test/utils/mock-fetch' export function makeClient(url?: string) { return new APIClient({ diff --git a/test/utils/mock-context.ts b/test/utils/mock-context.ts index d13b5d21..32bde04e 100644 --- a/test/utils/mock-context.ts +++ b/test/utils/mock-context.ts @@ -2,7 +2,7 @@ import {TransactContext} from '$lib' import {APIClient, FetchProvider, PermissionLevel} from '@greymass/eosio' import {mockUrl} from './mock-config' -import {mockFetch} from './mock-fetch' +import {mockFetch} from '$test/utils/mock-fetch' export function makeContext(): TransactContext { return new TransactContext({ diff --git a/test/utils/setup/accounts.ts b/test/utils/setup/accounts.ts index 173b9700..743198db 100644 --- a/test/utils/setup/accounts.ts +++ b/test/utils/setup/accounts.ts @@ -12,7 +12,7 @@ import { import {Buyrambytes, Delegatebw, Linkauth, Newaccount, Transfer, Updateauth} from './structs' // Mock of Fetch for debugging/testing -// import {mockFetch} from '../mock-fetch' +// import {mockFetch} from '$test/utils/mock-fetch' /** * THIS INFORMATION NEEDS TO BE POPULATED From 81f8bbd13cd4f273b07da9c9f328b3eecb923ba5 Mon Sep 17 00:00:00 2001 From: Aaron Cox Date: Tue, 17 Jan 2023 15:15:22 -0500 Subject: [PATCH 26/26] Allow either actor + permission or permissionLevel during Session instantiation Fixes #17 --- src/kit.ts | 15 +++++++- src/session.ts | 26 ++++++++++--- test/tests/session.ts | 84 +++++++++++++++++++++++++++++++++++++----- test/tests/transact.ts | 35 +++++++----------- 4 files changed, 123 insertions(+), 37 deletions(-) diff --git a/src/kit.ts b/src/kit.ts index 2e288d43..2576266c 100644 --- a/src/kit.ts +++ b/src/kit.ts @@ -8,7 +8,13 @@ import { PermissionLevelType, } from '@greymass/eosio' -import {Session, SessionOptions, WalletPlugin, WalletPluginLoginOptions} from './session' +import { + Session, + SessionOptions, + WalletPlugin, + WalletPluginContext, + WalletPluginLoginOptions, +} from './session' import { AbstractTransactPlugin, BaseTransactPlugin, @@ -178,10 +184,15 @@ export class SessionKit { walletPlugin: this.walletPlugins[0], } + const walletContext: WalletPluginContext = { + chain, + permissionLevel: PermissionLevel.from('eosio@active'), + } + const walletOptions: WalletPluginLoginOptions = { appName: this.appName, chains: this.chains, - context, + context: walletContext, } // Allow overriding of the default wallet plugin by specifying one in the options diff --git a/src/session.ts b/src/session.ts index 95a623e6..09173a87 100644 --- a/src/session.ts +++ b/src/session.ts @@ -2,6 +2,7 @@ import { APIClient, FetchProvider, Name, + NameType, PermissionLevel, PermissionLevelType, Signature, @@ -36,10 +37,15 @@ export interface WalletPluginOptions { name?: string } +export interface WalletPluginContext { + chain: ChainDefinition + permissionLevel: PermissionLevelType | string +} + export interface WalletPluginLoginOptions { appName: Name chains: ChainDefinition[] - context: SessionOptions + context: WalletPluginContext } export interface WalletPluginLoginResponse { @@ -61,12 +67,14 @@ export abstract class AbstractWalletPlugin implements WalletPlugin { * Options for creating a new instance of a [[Session]]. */ export interface SessionOptions { + actor?: NameType allowModify?: boolean broadcast?: boolean chain: ChainDefinitionType expireSeconds?: number fetch?: Fetch - permissionLevel: PermissionLevelType | string + permission?: NameType + permissionLevel?: PermissionLevelType | string transactPlugins?: AbstractTransactPlugin[] transactPluginsOptions?: TransactPluginsOptions walletPlugin: WalletPlugin @@ -108,15 +116,23 @@ export class Session { if (options.transactPluginsOptions) { this.transactPluginsOptions = options.transactPluginsOptions } - this.permissionLevel = PermissionLevel.from(options.permissionLevel) + if (options.permissionLevel) { + this.permissionLevel = PermissionLevel.from(options.permissionLevel) + } else if (options.actor && options.permission) { + this.permissionLevel = PermissionLevel.from(`${options.actor}@${options.permission}`) + } else { + throw new Error( + 'Either a permissionLevel or actor/permission must be provided when creating a new Session.' + ) + } this.wallet = options.walletPlugin } - get accountName(): Name { + get actor(): Name { return this.permissionLevel.actor } - get permissionName(): Name { + get permission(): Name { return this.permissionLevel.permission } diff --git a/test/tests/session.ts b/test/tests/session.ts index e68dab71..5210e779 100644 --- a/test/tests/session.ts +++ b/test/tests/session.ts @@ -1,7 +1,7 @@ import {assert} from 'chai' import SessionKit, {BaseTransactPlugin, ChainDefinition, Session, SessionOptions} from '$lib' -import {PermissionLevel, TimePointSec} from '@greymass/eosio' +import {Name, PermissionLevel, TimePointSec} from '@greymass/eosio' import {mockFetch} from '$test/utils/mock-fetch' import {MockTransactPlugin, MockTransactResourceProviderPlugin} from '$test/utils/mock-hook' @@ -148,6 +148,76 @@ suite('session', function () { ) }) }) + suite('authority', function () { + suite('actor + permission', function () { + test('typed values', async function () { + const testSession = new Session({ + actor: Name.from('account'), + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permission: Name.from('permission'), + walletPlugin: wallet, + }) + assert.instanceOf(testSession, Session) + }) + test('untyped values', async function () { + const testSession = new Session({ + actor: 'account', + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permission: 'permission', + walletPlugin: wallet, + }) + assert.instanceOf(testSession, Session) + }) + }) + suite('permissionLevel', function () { + test('typed values', async function () { + const testSession = new Session({ + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permissionLevel: PermissionLevel.from('account@permission'), + walletPlugin: wallet, + }) + assert.instanceOf(testSession, Session) + }) + test('untyped values', async function () { + const testSession = new Session({ + actor: 'account', + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + permissionLevel: 'account@permission', + walletPlugin: wallet, + }) + assert.instanceOf(testSession, Session) + }) + }) + test('undefined', function () { + assert.throws(() => { + new Session({ + actor: 'account', + chain: ChainDefinition.from({ + id: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d', + url: 'https://jungle4.greymass.com', + }), + fetch: mockFetch, // Required for unit tests + walletPlugin: wallet, + }) + }) + }) + }) suite('passed as', function () { test('typed values', async function () { const testSession = new Session({ @@ -233,13 +303,9 @@ suite('session', function () { }) }) test('getters', function () { - assert.equal( - session.accountName, - PermissionLevel.from(mockSessionOptions.permissionLevel).actor - ) - assert.equal( - session.permissionName, - PermissionLevel.from(mockSessionOptions.permissionLevel).permission - ) + const expectedPermission = PermissionLevel.from(mockPermissionLevel) + // Ensure transaction authority was templated + assert.isTrue(session.actor.equals(expectedPermission.actor)) + assert.isTrue(session.permission.equals(expectedPermission.permission)) }) }) diff --git a/test/tests/transact.ts b/test/tests/transact.ts index e711bbf0..fff8df0e 100644 --- a/test/tests/transact.ts +++ b/test/tests/transact.ts @@ -1,7 +1,7 @@ import {assert} from 'chai' import zlib from 'pako' -import {PermissionLevel, Serializer, Signature, TimePointSec} from '@greymass/eosio' +import {Action, Name, PermissionLevel, Serializer, Signature, TimePointSec} from '@greymass/eosio' import {ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request' import SessionKit, { @@ -24,6 +24,7 @@ import { import {makeMockAction, makeMockActions, makeMockTransaction} from '$test/utils/mock-transfer' import {makeWallet} from '$test/utils/mock-wallet' import {mockPermissionLevel} from '$test/utils/mock-config' +import {Transfer} from '$test/utils/setup/structs' const client = makeClient() const wallet = makeWallet() @@ -562,20 +563,14 @@ suite('transact', function () { }) assert.exists(result.transaction) if (result.transaction) { + const resolvedPermission = result.transaction.actions[0].authorization[0] + const resolvedData = Transfer.from(result.transaction.actions[0].data) + const expectedPermission = PermissionLevel.from(mockPermissionLevel) // Ensure transaction authority was templated - assert.equal( - result.transaction.actions[0].authorization[0].actor, - PermissionLevel.from(mockSessionOptions.permissionLevel).actor - ) - assert.equal( - result.transaction.actions[0].authorization[0].permission, - PermissionLevel.from(mockSessionOptions.permissionLevel).permission - ) + assert.isTrue(resolvedPermission.actor.equals(expectedPermission.actor)) + assert.isTrue(resolvedPermission.permission.equals(expectedPermission.permission)) // Ensure transaction data was templated - assert.equal( - result.transaction.actions[0].data.from, - PermissionLevel.from(mockSessionOptions.permissionLevel).actor - ) + assert.isTrue(resolvedData.from.equals(expectedPermission.actor)) } else { assert.fail('Decoded transaction was not returned in result.') } @@ -591,14 +586,12 @@ suite('transact', function () { assert.exists(result.resolved) const {resolved} = result // Ensure it returns resolved request with authority templated - assert.equal( - resolved?.transaction.actions[0].authorization[0].actor, - PermissionLevel.from(mockSessionOptions.permissionLevel).actor - ) - assert.equal( - resolved?.transaction.actions[0].authorization[0].permission, - PermissionLevel.from(mockSessionOptions.permissionLevel).permission - ) + if (resolved) { + const resolvedPermission = resolved.transaction.actions[0].authorization[0] + const expectedPermission = PermissionLevel.from(mockPermissionLevel) + assert.isTrue(resolvedPermission.actor.equals(expectedPermission.actor)) + assert.isTrue(resolvedPermission.permission.equals(expectedPermission.permission)) + } }) test('valid signatures', async function () { const {action, session} = await mockData()