mirror of
https://github.com/wharfkit/session.git
synced 2026-07-21 10:02:11 +00:00
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:
+2
-1
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
lib/
|
||||
build/
|
||||
build/
|
||||
notes.md
|
||||
|
||||
+33
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export * from './abi'
|
||||
export * from './kit'
|
||||
export * from './plugins'
|
||||
export * from './session'
|
||||
export * from './transact'
|
||||
export * from './types'
|
||||
|
||||
+22
-8
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||
|
||||
@@ -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]))
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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}`)
|
||||
}
|
||||
@@ -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,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,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'),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -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,4 +1,5 @@
|
||||
import {Action, API, Asset, Name, Struct, Transaction} from '@greymass/eosio'
|
||||
|
||||
import {mockAccountName, mockPermissionName} from './mock-config'
|
||||
|
||||
@Struct.type('transfer')
|
||||
|
||||
@@ -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 | ✅ | ✅ | ✅ | ✅ |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user