0.2.0 - Release (#20)

* 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.

* Added default esrOptions to the TransactContext

Resolves #15.

This gives developers a default set of options when creating ESR requests.

* Renamed Context `session` to `permissionLevel`

* Renamed getters to be more specific

Motivation was that I imagine we'll want the `.account` getter to return an Account Kit instance.

* Added getters to the TransactContext

* Deduplicating code by adding a helper function

* Added the ABICache and implemented in the TransactContext

* Removed unused import

* Allow specifying `expireSeconds` throughout the stack

Resolves #22

* An ENV variable to dump http request traffic for debugging

* Add creation of a wharfkitnoop account for cosigning tests

* Ignore any local notes

* Refector TransactContext and ABICache

* Allow context to resolve transactions (ease for developers)

* Retain a revision history of request modifications in the TransactResult

This will

* Removed unused import

* Fix for nodejs v14

Cannot use the at method in older nodejs versions

* Manually clone incoming SigningRequest

This is to preserve all data and override the zlib/abiProvider values.

* Migrate plugin tests into new suite

* Split cloneRequest into its own function

* 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.

* Syntax

* Adding an appendAction and prependAction helper

* Finished testing resolve

* Fixed browser test runner

* Allow either actor + permission or permissionLevel during Session instantiation

Fixes #17
This commit is contained in:
Aaron Cox
2023-01-17 15:20:46 -05:00
committed by GitHub
parent 83b598de7f
commit 26f4deabf6
27 changed files with 1232 additions and 351 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
node_modules/
lib/
build/
build/
notes.md
+33
View File
@@ -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<string, ABI> = new Map()
public readonly pending: Map<string, Promise<API.v1.GetAbiResponse>> = new Map()
constructor(public readonly client: APIClient) {}
public async getAbi(account: Name): Promise<ABI> {
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
}
}
+2
View File
@@ -1,4 +1,6 @@
export * from './abi'
export * from './kit'
export * from './plugins'
export * from './session'
export * from './transact'
export * from './types'
+22 -8
View File
@@ -8,18 +8,20 @@ import {
PermissionLevelType,
} from '@greymass/eosio'
import {ChainDefinition, ChainDefinitionType, Fetch} from './types'
import {
Session,
SessionOptions,
WalletPlugin,
WalletPluginContext,
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',
@@ -96,6 +98,7 @@ export interface LoginOptions {
export interface SessionKitOptions {
appName: NameType
chains: ChainDefinitionType[]
expireSeconds?: number
fetch?: Fetch
loginPlugins?: LoginPlugin[]
transactPlugins?: TransactPlugin[]
@@ -109,6 +112,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 +123,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 +176,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,
@@ -175,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
+143 -198
View File
@@ -1,11 +1,8 @@
import {
ABIDef,
AnyAction,
AnyTransaction,
APIClient,
Checksum256Type,
FetchProvider,
Name,
NameType,
PermissionLevel,
PermissionLevelType,
Signature,
@@ -13,49 +10,42 @@ import {
} from '@greymass/eosio'
import {
AbiProvider,
RequestDataV2,
RequestDataV3,
RequestSignature,
ResolvedSigningRequest,
ResolvedTransaction,
SigningRequest,
} from 'eosio-signing-request'
import zlib from 'pako'
import {ABICache} from './abi'
import {
AbstractTransactPlugin,
BaseTransactPlugin,
TransactArgs,
TransactContext,
TransactOptions,
TransactPlugin,
TransactPluginsOptions,
TransactResult,
TransactRevisions,
} from './transact'
import {ChainDefinition, ChainDefinitionType, Fetch} from './types'
import {getFetch} from './utils'
export type TransactPluginsOptions = Record<string, unknown>
export enum TransactHookTypes {
beforeSign = 'beforeSign',
afterSign = 'afterSign',
beforeBroadcast = 'beforeBroadcast',
afterBroadcast = 'afterBroadcast',
}
export type TransactHook = (
request: SigningRequest,
context: TransactContext
) => Promise<TransactHookResponse>
export interface TransactHooks {
afterSign: TransactHook[]
beforeSign: TransactHook[]
afterBroadcast: TransactHook[]
beforeBroadcast: TransactHook[]
}
export interface TransactHookResponse {
request: SigningRequest
signatures?: Signature[]
}
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 {
@@ -73,155 +63,33 @@ 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 {
client: APIClient
fetch: Fetch
session: 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 client: APIClient
readonly fetch: Fetch
readonly hooks: TransactHooks = {
afterBroadcast: [],
afterSign: [],
beforeBroadcast: [],
beforeSign: [],
}
readonly session: PermissionLevel
readonly transactPluginsOptions: TransactPluginsOptions
constructor(options: TransactContextOptions) {
this.client = options.client
this.fetch = options.fetch
this.session = options.session
this.transactPluginsOptions = options.transactPluginsOptions || {}
options.transactPlugins?.forEach((plugin: AbstractTransactPlugin) => {
plugin.register(this)
})
}
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
/**
* 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]].
*/
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
}
export class Session {
readonly abiCache = ABICache
readonly allowModify: boolean = true
readonly broadcast: boolean = true
readonly chain: ChainDefinition
readonly expireSeconds: number = 120
readonly fetch: Fetch
readonly permissionLevel: PermissionLevel
readonly transactPlugins: TransactPlugin[]
readonly transactPluginsOptions: TransactPluginsOptions = {}
readonly permissionLevel: PermissionLevel
readonly wallet: WalletPlugin
constructor(options: SessionOptions) {
@@ -232,6 +100,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 {
@@ -245,11 +116,19 @@ 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 account(): Name {
get actor(): Name {
return this.permissionLevel.actor
}
@@ -288,69 +167,134 @@ export class Session {
return args
}
async createRequest(args: TransactArgs): Promise<SigningRequest> {
const abiProvider: AbiProvider = {
getAbi: async (account: Name): Promise<ABIDef> => {
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
},
/**
* 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
}
/**
* 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<SigningRequest> {
let request: SigningRequest
const options = {
abiProvider,
zlib,
}
if (args.request && args.request instanceof SigningRequest) {
return SigningRequest.from(String(args.request), options)
request = this.cloneRequest(args.request, abiProvider)
} 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
}
/**
* 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<SigningRequest> {
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.
*
* @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 --> E{{"Hook(s): beforeBroadcast"}}
* E --> F[Broadcast Plugin]
* F --> G{{"Hook(s): afterBroadcast"}}
* G --> H[TransactResult]
* D --> E[Broadcast Plugin]
* E --> F{{"Hook(s): afterBroadcast"}}
* F --> G[TransactResult]
*/
async transact(args: TransactArgs, options?: TransactOptions): Promise<TransactResult> {
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,
transactPlugins: options?.transactPlugins || this.transactPlugins,
transactPluginsOptions: options?.transactPluginsOptions || this.transactPluginsOptions,
session: this.permissionLevel,
})
// Process TransactArgs and convert to a SigningRequest
const request: SigningRequest = await this.createRequest(args)
let request: SigningRequest = await this.createRequest(args, abiCache)
// Create response template to this transact call
const result: TransactResult = {
chain: this.chain,
request,
resolved: undefined,
revisions: new TransactRevisions(request),
signatures: [],
signer: this.permissionLevel,
transaction: undefined,
@@ -362,16 +306,25 @@ 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
// Run the `beforeSign` hooks
for (const hook of context.hooks.beforeSign) {
const response = await hook(result.request.clone(), context)
// TODO: Verify we should be cloning the requests here, and write tests to verify they cannot be modified
// Get the response of the hook by passing a clonied request.
const response = await hook(request.clone(), context)
// 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) {
result.request = response.request.clone()
request = await this.updateRequest(request, response.request, abiCache)
}
// If signatures were returned, append them
if (response.signatures) {
@@ -379,14 +332,9 @@ 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() // TODO: ABI Cache Implementation
// 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
@@ -398,10 +346,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,
@@ -419,3 +363,4 @@ export class Session {
return result
}
}
export {AbstractTransactPlugin}
+263
View File
@@ -0,0 +1,263 @@
import {
AnyAction,
AnyTransaction,
APIClient,
Checksum256Type,
Name,
PermissionLevel,
Serializer,
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<string, unknown>
export enum TransactHookTypes {
beforeSign = 'beforeSign',
afterSign = 'afterSign',
afterBroadcast = 'afterBroadcast',
}
export type TransactHook = (
request: SigningRequest,
context: TransactContext
) => Promise<TransactHookResponse>
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)
}
async resolve(request: SigningRequest, expireSeconds = 120): Promise<ResolvedSigningRequest> {
// 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.
* 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
/**
* 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[this.revisions.length - 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) : [],
},
})
}
}
/**
* 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}
/** An array containing revisions of the transaction as modified by plugins as ESR payloads */
revisions: TransactRevisions
/** 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')
}
}
+38 -1
View File
@@ -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
}
+4 -1
View File
@@ -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'}),
+58 -4
View File
@@ -1,9 +1,63 @@
import {makeClient} from '$test/utils/mock-client'
import {assert} from 'chai'
const client = makeClient()
import {ABI, Checksum256, Name, PermissionLevel, Transaction} 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()
suite('context', function () {
suite('pre-sign', function () {
test('prepend action on `action`', async function () {})
suite('abiProvider', function () {
test('has default', function () {
assert.isDefined(context.abiCache)
})
test('fetches 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')
})
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')
})
})
suite('esrOptions', function () {
test('has abiProvider', function () {
assert.isDefined(context.esrOptions.abiProvider)
assert.isFunction(context.esrOptions.abiProvider?.getAbi)
})
test('has zlib', function () {
assert.isDefined(context.esrOptions.zlib)
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 = 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)
})
})
})
-2
View File
@@ -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
+35 -1
View File
@@ -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)
@@ -1,7 +0,0 @@
import {assert} from 'chai'
export const beforeBroadcastHooks = () => {
suite('beforeBroadcast', function () {
test('TODO', async function () {})
})
}
@@ -76,7 +76,7 @@ export class MockTransactResourceProviderPlugin extends AbstractTransactPlugin {
body: JSON.stringify({
ref: 'unittest',
request,
signer: context.session,
signer: context.permissionLevel,
}),
})
@@ -125,25 +125,10 @@ export class MockTransactResourceProviderPlugin extends AbstractTransactPlugin {
response: ResourceProviderResponse,
context: TransactContext
): Promise<SigningRequest> {
// Establish an AbiProvider based on the session context.
const abiProvider: AbiProvider = {
getAbi: async (account: Name): Promise<ABIDef> => {
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.
@@ -167,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.')
}
}
+118 -9
View File
@@ -1,7 +1,7 @@
import {assert} from 'chai'
import SessionKit, {BaseTransactPlugin, ChainDefinition, Session, SessionOptions} from '$lib'
import {PermissionLevel} 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'
@@ -105,6 +105,119 @@ 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('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({
@@ -190,13 +303,9 @@ suite('session', function () {
})
})
test('getters', function () {
assert.equal(
session.account,
PermissionLevel.from(mockSessionOptions.permissionLevel).actor
)
assert.equal(
session.permission,
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))
})
})
+164 -31
View File
@@ -1,10 +1,11 @@
import {assert} from 'chai'
import zlib from 'pako'
import {PermissionLevel, Serializer, Signature} from '@greymass/eosio'
import {Action, Name, PermissionLevel, Serializer, Signature, TimePointSec} from '@greymass/eosio'
import {ResolvedSigningRequest, SigningRequest} from 'eosio-signing-request'
import SessionKit, {
ABICache,
ChainDefinition,
Session,
SessionOptions,
@@ -14,10 +15,16 @@ 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 {
mockMetadataFooWriterPlugin,
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'
import {Transfer} from '$test/utils/setup/structs'
const client = makeClient()
const wallet = makeWallet()
@@ -134,6 +141,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({
@@ -144,6 +165,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 () {
@@ -236,6 +278,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()
@@ -284,7 +369,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)
},
}
@@ -303,14 +387,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)
})
})
suite('transactPluginsOptions', function () {
test('transact', async function () {
@@ -406,6 +482,71 @@ 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.')
}
})
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 () {
const {session, transaction} = await mockData()
@@ -422,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.')
}
@@ -451,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()
+150
View File
@@ -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]))
})
})
})
+26
View File
@@ -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}`)
}
-27
View File
@@ -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}`)
}
}
+1 -1
View File
@@ -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({
+2 -2
View File
@@ -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({
@@ -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'),
})
}
+22
View File
@@ -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,
}
}
+3
View File
@@ -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)
+46 -25
View File
@@ -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
@@ -27,11 +28,16 @@ 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)
}
}
// 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
@@ -46,13 +52,6 @@ export async function mockTransactResourceProviderPresignHook(
signatures: [],
}
}
// 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',
@@ -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: [],
}
}
@@ -92,3 +76,40 @@ 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
),
})),
}
export const mockMetadataFooWriterPlugin = {
register: (context) =>
context.addHook(TransactHookTypes.beforeSign, async (request) => {
request.setInfoKey('foo', 'baz')
return {
request,
}
}),
}
+20
View File
@@ -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,
}
+1
View File
@@ -1,4 +1,5 @@
import {Action, API, Asset, Name, Struct, Transaction} from '@greymass/eosio'
import {mockAccountName, mockPermissionName} from './mock-config'
@Struct.type('transfer')
+15 -14
View File
@@ -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 | ✅ | ✅ | ✅ | ✅ |
+61 -1
View File
@@ -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
@@ -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<Transac
}),
}),
]
if (account.name === 'wharfkitnoop') {
actions.push(
// Add the test permission to the account
Action.from({
account: 'eosio',
name: 'updateauth',
authorization: [
{
actor: account.name,
permission: 'active',
},
],
data: Updateauth.from({
account: account.name,
permission: 'cosign',
parent: 'active',
auth: {
threshold: 1,
keys: [
{
key: testKey,
weight: 1,
},
],
accounts: [],
waits: [],
},
}),
})
)
actions.push(
// Link the test permission to the eosio.token::transfer action
Action.from({
account: 'eosio',
name: 'linkauth',
authorization: [
{
actor: account.name,
permission: 'active',
},
],
data: Linkauth.from({
account: account.name,
code: 'greymassnoop',
type: 'noop',
requirement: 'cosign',
}),
})
)
}
const result = await session.transact({actions})
return result
}