-
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathsession.ts
More file actions
417 lines (377 loc) · 15.1 KB
/
session.ts
File metadata and controls
417 lines (377 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
import {
APIClient,
FetchProvider,
Name,
NameType,
PermissionLevel,
PermissionLevelType,
Serializer,
Signature,
SignedTransaction,
} from '@greymass/eosio'
import {
AbiProvider,
RequestDataV2,
RequestDataV3,
RequestSignature,
ResolvedSigningRequest,
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 interface WalletPluginOptions {
name?: string
}
export interface WalletPluginContext {
chain: ChainDefinition
permissionLevel: PermissionLevelType | string
}
export interface WalletPluginLoginOptions {
appName: Name
chains: ChainDefinition[]
context: WalletPluginContext
}
export interface WalletPluginLoginResponse {
chain: ChainDefinition
permissionLevel: PermissionLevel
}
export interface WalletPlugin {
login(options: WalletPluginLoginOptions): WalletPluginLoginResponse
sign(chain: ChainDefinition, transaction: ResolvedSigningRequest): Signature
}
export abstract class AbstractWalletPlugin implements WalletPlugin {
public abstract login(options: WalletPluginLoginOptions): WalletPluginLoginResponse
public abstract sign(chain: ChainDefinition, transaction: ResolvedSigningRequest): Signature
}
/**
* Options for creating a new instance of a [[Session]].
*/
export interface SessionOptions {
actor?: NameType
allowModify?: boolean
broadcast?: boolean
chain: ChainDefinitionType
expireSeconds?: number
fetch?: Fetch
permission?: NameType
permissionLevel?: PermissionLevelType | string
transactPlugins?: AbstractTransactPlugin[]
transactPluginsOptions?: TransactPluginsOptions
validatePluginSignatures?: boolean
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 validatePluginSignatures: boolean = true
readonly wallet: WalletPlugin
constructor(options: SessionOptions) {
this.chain = ChainDefinition.from(options.chain)
if (options.allowModify !== undefined) {
this.allowModify = options.allowModify
}
if (options.broadcast !== undefined) {
this.broadcast = options.broadcast
}
if (options.expireSeconds) {
this.expireSeconds = options.expireSeconds
}
if (options.fetch) {
this.fetch = options.fetch
} else {
this.fetch = getFetch(options)
}
if (options.transactPlugins) {
this.transactPlugins = options.transactPlugins
} else {
this.transactPlugins = [new BaseTransactPlugin()]
}
if (options.transactPluginsOptions) {
this.transactPluginsOptions = options.transactPluginsOptions
}
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.'
)
}
if (options.validatePluginSignatures !== undefined) {
this.validatePluginSignatures = options.validatePluginSignatures
}
this.wallet = options.walletPlugin
}
get actor(): Name {
return this.permissionLevel.actor
}
get permission(): Name {
return this.permissionLevel.permission
}
get client(): APIClient {
return new APIClient({provider: new FetchProvider(this.chain.url, {fetch: this.fetch})})
}
upgradeTransaction(args) {
// eosjs transact compat: upgrade to transaction if args have any header fields
const anyArgs = args as any
if (
args.actions &&
(anyArgs.expiration ||
anyArgs.ref_block_num ||
anyArgs.ref_block_prefix ||
anyArgs.max_net_usage_words ||
anyArgs.max_cpu_usage_ms ||
anyArgs.delay_sec)
) {
return (args = {
transaction: {
expiration: '1970-01-01T00:00:00',
ref_block_num: 0,
ref_block_prefix: 0,
max_net_usage_words: 0,
max_cpu_usage_ms: 0,
delay_sec: 0,
...anyArgs,
},
})
}
return args
}
/**
* 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) {
request = this.cloneRequest(args.request, abiProvider)
} else if (args.request) {
request = SigningRequest.from(args.request, options)
} else {
args = this.upgradeTransaction(args)
request = await SigningRequest.create(
{
...args,
chainId: this.chain.id,
},
options
)
}
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[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,
})
// Process TransactArgs and convert to a SigningRequest
let request: SigningRequest = await this.createRequest(args, abiCache)
// Create response template to this transact call
const result: TransactResult = {
chain: this.chain,
keys: [],
request,
resolved: undefined,
revisions: new TransactRevisions(request),
signatures: [],
signer: this.permissionLevel,
transaction: undefined,
}
// Whether or not the request should be able to be modified by beforeSign hooks
const allowModify =
options && typeof options.allowModify !== 'undefined'
? 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
// Whether all signatures generated
const willValidatePluginSignatures =
options && typeof options.validatePluginSignatures !== 'undefined'
? options.validatePluginSignatures
: this.validatePluginSignatures
// Run the `beforeSign` hooks
for (const hook of context.hooks.beforeSign) {
// Get the response of the hook by passing a clonied request.
const response = await hook(request.clone(), context)
// Save revision history for developers to debug modifications to requests.
result.revisions.addRevision(response, String(hook), allowModify)
// If modification is allowed, change the current request.
if (allowModify) {
request = await this.updateRequest(request, response.request, abiCache)
}
// If signatures were returned, record them in the response.
if (response.signatures?.length) {
// Merge new signatures alongside existing signatures into the TransactResult.
result.signatures = [...result.signatures, ...response.signatures]
// Recover the keys used to generate the signatures at the time of the request.
const recoveredKeys = response.signatures.map((signature) => {
const requestTransaction = request.getRawTransaction()
const requestDigest = requestTransaction.signingDigest(this.chain.id)
return signature.recoverDigest(requestDigest)
})
// Merge newly discovered keys into the TransactResult.
result.keys = [...result.keys, ...recoveredKeys]
}
}
// Validate all the signatures returned by the plugins against the current request
if (willValidatePluginSignatures) {
this.validateBeforeSignSignatures(context, request, result)
}
// 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
const signature = await this.wallet.sign(this.chain, result.resolved)
result.signatures.push(signature)
// Run the `afterSign` hooks
for (const hook of context.hooks.afterSign) await hook(result.request.clone(), context)
// Broadcast transaction if requested
if (willBroadcast) {
// Assemble the signed transaction to broadcast
const signed = SignedTransaction.from({
...result.resolved.transaction,
signatures: result.signatures,
})
// Broadcast the signed transaction
result.response = await context.client.v1.chain.send_transaction(signed)
// Run the `afterBroadcast` hooks
for (const hook of context.hooks.afterBroadcast)
await hook(result.request.clone(), context)
}
return result
}
validateBeforeSignSignatures(
context: TransactContext,
request: SigningRequest,
result: TransactResult
): void {
const requestTransaction = request.getRawTransaction()
const requestDigest = requestTransaction.signingDigest(this.chain.id)
const publicKeys = Serializer.objectify(result.keys)
result.signatures.forEach((signature) => {
const recoveredKey = signature.recoverDigest(requestDigest)
const verified = signature.verifyDigest(requestDigest, recoveredKey)
if (!verified || !publicKeys.includes(String(recoveredKey))) {
throw new Error(
`A signature (${signature}) provided by a beforeSign hook using ` +
`a key (${recoveredKey}) has been invalidated, likely due to the transaction ` +
`being modified by another hook after the signature was created. To disable ` +
`this error, set validatePluginSignatures equal to false.`
)
}
})
}
}
export {AbstractTransactPlugin}