@wharfkit/transact-plugin-resource-provider 0.3.0-ui-5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +29 -0
- package/README.md +33 -0
- package/lib/resource-provider-plugin.d.ts +84 -0
- package/lib/resource-provider-plugin.js +322 -0
- package/lib/resource-provider-plugin.js.map +1 -0
- package/lib/resource-provider-plugin.m.js +302 -0
- package/lib/resource-provider-plugin.m.js.map +1 -0
- package/lib/resource-provider-plugin.umd.js +303 -0
- package/lib/transact-plugin-resource-provider.d.ts +84 -0
- package/lib/transact-plugin-resource-provider.js +322 -0
- package/lib/transact-plugin-resource-provider.js.map +1 -0
- package/lib/transact-plugin-resource-provider.m.js +302 -0
- package/lib/transact-plugin-resource-provider.m.js.map +1 -0
- package/package.json +60 -0
- package/src/index.ts +289 -0
- package/src/utils.ts +53 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AbstractTransactPlugin,
|
|
3
|
+
Action,
|
|
4
|
+
Asset,
|
|
5
|
+
AssetType,
|
|
6
|
+
ChainDefinition,
|
|
7
|
+
Name,
|
|
8
|
+
Serializer,
|
|
9
|
+
Signature,
|
|
10
|
+
SigningRequest,
|
|
11
|
+
Struct,
|
|
12
|
+
TransactContext,
|
|
13
|
+
TransactHookResponse,
|
|
14
|
+
TransactHookTypes,
|
|
15
|
+
Transaction,
|
|
16
|
+
} from '@wharfkit/session'
|
|
17
|
+
|
|
18
|
+
import {getNewActions, hasOriginalActions} from './utils'
|
|
19
|
+
|
|
20
|
+
interface ResourceProviderOptions {
|
|
21
|
+
allowFees?: boolean
|
|
22
|
+
// allowActions?: NameType[]
|
|
23
|
+
endpoints: Record<string, string>
|
|
24
|
+
maxFee?: AssetType
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ResourceProviderResponseData {
|
|
28
|
+
request: [string, object]
|
|
29
|
+
signatures: string[]
|
|
30
|
+
version: unknown
|
|
31
|
+
fee?: AssetType
|
|
32
|
+
costs?: {
|
|
33
|
+
cpu: AssetType
|
|
34
|
+
net: AssetType
|
|
35
|
+
ram: AssetType
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ResourceProviderResponse {
|
|
40
|
+
code: number
|
|
41
|
+
data: ResourceProviderResponseData
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Struct.type('transfer')
|
|
45
|
+
export class Transfer extends Struct {
|
|
46
|
+
@Struct.field(Name) from!: Name
|
|
47
|
+
@Struct.field(Name) to!: Name
|
|
48
|
+
@Struct.field(Asset) quantity!: Asset
|
|
49
|
+
@Struct.field('string') memo!: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default class ResourceProviderPlugin extends AbstractTransactPlugin {
|
|
53
|
+
readonly allowFees: boolean = false
|
|
54
|
+
// readonly allowActions: Name[] = [
|
|
55
|
+
// Name.from('eosio.token:transfer'),
|
|
56
|
+
// Name.from('eosio:buyrambytes'),
|
|
57
|
+
// ]
|
|
58
|
+
readonly maxFee?: Asset
|
|
59
|
+
|
|
60
|
+
readonly endpoints: Record<string, string> = {}
|
|
61
|
+
|
|
62
|
+
constructor(options: ResourceProviderOptions) {
|
|
63
|
+
super()
|
|
64
|
+
// Set the endpoints and chains available
|
|
65
|
+
this.endpoints = options.endpoints
|
|
66
|
+
if (typeof options?.allowFees !== 'undefined') {
|
|
67
|
+
this.allowFees = options.allowFees
|
|
68
|
+
}
|
|
69
|
+
// TODO: Allow contact/action combos to be passed in and checked against to ensure no rogue actions were appended.
|
|
70
|
+
// if (typeof options.allowActions !== 'undefined') {
|
|
71
|
+
// this.allowActions = options.allowActions.map((action) => Name.from(action))
|
|
72
|
+
// }
|
|
73
|
+
if (typeof options?.maxFee !== 'undefined') {
|
|
74
|
+
this.maxFee = Asset.from(options.maxFee)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
register(context: TransactContext): void {
|
|
79
|
+
context.addHook(TransactHookTypes.beforeSign, (request, context) =>
|
|
80
|
+
this.request(request, context)
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getEndpoint(chain: ChainDefinition): string {
|
|
85
|
+
return this.endpoints[String(chain.id)]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async request(
|
|
89
|
+
request: SigningRequest,
|
|
90
|
+
context: TransactContext
|
|
91
|
+
): Promise<TransactHookResponse> {
|
|
92
|
+
// Validate that this request is valid for the resource provider
|
|
93
|
+
this.validateRequest(request, context)
|
|
94
|
+
|
|
95
|
+
// Determine appropriate URL for this request
|
|
96
|
+
const endpoint = this.getEndpoint(context.chain)
|
|
97
|
+
|
|
98
|
+
// If no endpoint was found, gracefully fail and return the original request.
|
|
99
|
+
if (!endpoint) {
|
|
100
|
+
return {
|
|
101
|
+
request,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Assemble the request to the resource provider.
|
|
106
|
+
const url = `${endpoint}/v1/resource_provider/request_transaction`
|
|
107
|
+
|
|
108
|
+
// Perform the request to the resource provider.
|
|
109
|
+
const response = await context.fetch(url, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
ref: 'unittest',
|
|
113
|
+
request,
|
|
114
|
+
signer: context.permissionLevel,
|
|
115
|
+
}),
|
|
116
|
+
})
|
|
117
|
+
const json: ResourceProviderResponse = await response.json()
|
|
118
|
+
|
|
119
|
+
// If the resource provider refused to process this request, return the original request without modification.
|
|
120
|
+
if (response.status === 400) {
|
|
121
|
+
return {
|
|
122
|
+
request,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const requiresPayment = response.status === 402
|
|
127
|
+
if (requiresPayment) {
|
|
128
|
+
// If the resource provider offered transaction with a fee, but plugin doesn't allow fees, return the original transaction.
|
|
129
|
+
if (!this.allowFees) {
|
|
130
|
+
// TODO: Notify the script somehow of this, maybe we need an optional logger?
|
|
131
|
+
// Notify that a fee was required but not allowed via allowFees: false.
|
|
132
|
+
return {
|
|
133
|
+
request,
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Retrieve the transaction from the response
|
|
139
|
+
const modifiedTransaction = this.getModifiedTransaction(json)
|
|
140
|
+
// Ensure the new transaction has an unmodified version of the original action(s)
|
|
141
|
+
const originalActionsIntact = hasOriginalActions(
|
|
142
|
+
request.getRawTransaction(),
|
|
143
|
+
modifiedTransaction
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if (!originalActionsIntact) {
|
|
147
|
+
// TODO: Notify the script somehow of this, maybe we need an optional logger?
|
|
148
|
+
// Notify that the original actions requested were modified somehow, and reject the modification.
|
|
149
|
+
return {
|
|
150
|
+
request,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Retrieve all newly appended actions from the modified transaction
|
|
155
|
+
const addedActions = getNewActions(request.getRawTransaction(), modifiedTransaction)
|
|
156
|
+
|
|
157
|
+
// TODO: Check that all the addedActions are allowed via this.allowActions
|
|
158
|
+
|
|
159
|
+
// Find any transfer actions that were added to the transaction, which we assume are fees
|
|
160
|
+
const addedFees = addedActions
|
|
161
|
+
.filter(
|
|
162
|
+
(action: Action) =>
|
|
163
|
+
action.account.equals('eosio.token') && action.name.equals('transfer')
|
|
164
|
+
)
|
|
165
|
+
.map(
|
|
166
|
+
(action: Action) =>
|
|
167
|
+
Serializer.decode({
|
|
168
|
+
data: action.data,
|
|
169
|
+
type: Transfer,
|
|
170
|
+
}).quantity
|
|
171
|
+
)
|
|
172
|
+
.reduce((total: Asset, fee: Asset) => {
|
|
173
|
+
total.units.add(fee.units)
|
|
174
|
+
return total
|
|
175
|
+
}, Asset.from('0.0000 EOS'))
|
|
176
|
+
|
|
177
|
+
// If the resource provider offered transaction with a fee, but the fee was higher than allowed, return the original transaction.
|
|
178
|
+
if (this.maxFee) {
|
|
179
|
+
if (addedFees.units > this.maxFee.units) {
|
|
180
|
+
// TODO: Notify the script somehow of this, maybe we need an optional logger?
|
|
181
|
+
// Notify that a fee was required but higher than allowed via maxFee.
|
|
182
|
+
return {
|
|
183
|
+
request,
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Validate that the response is valid for the session.
|
|
189
|
+
await this.validateResponseData(json)
|
|
190
|
+
|
|
191
|
+
// NYI: Interact with interface via context for fee based prompting
|
|
192
|
+
|
|
193
|
+
/* Psuedo-code for fee based prompting
|
|
194
|
+
|
|
195
|
+
if (response.status === 402) {
|
|
196
|
+
|
|
197
|
+
// Prompt for the fee acceptance
|
|
198
|
+
const promptResponse = context.userPrompt({
|
|
199
|
+
title: 'Transaction Fee Required',
|
|
200
|
+
message: `This transaction requires a fee of ${response.json.data.fee} EOS. Do you wish to accept this fee?`,
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
// If the user did not accept the fee, return the original request without modification.
|
|
204
|
+
if (!promptResponse) {
|
|
205
|
+
return {
|
|
206
|
+
request,
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
} */
|
|
210
|
+
|
|
211
|
+
// Create a new signing request based on the response to return to the session's transact flow.
|
|
212
|
+
const modified = await this.createRequest(json, context)
|
|
213
|
+
|
|
214
|
+
// Return the modified transaction and additional signatures
|
|
215
|
+
return {
|
|
216
|
+
request: modified,
|
|
217
|
+
signatures: json.data.signatures.map((sig) => Signature.from(sig)),
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
getModifiedTransaction(json): Transaction {
|
|
222
|
+
switch (json.data.request[0]) {
|
|
223
|
+
case 'action':
|
|
224
|
+
throw new Error('A resource provider providing an "action" is not supported.')
|
|
225
|
+
case 'actions':
|
|
226
|
+
throw new Error('A resource provider providing "actions" is not supported.')
|
|
227
|
+
case 'transaction':
|
|
228
|
+
return Transaction.from(json.data.request[1])
|
|
229
|
+
}
|
|
230
|
+
throw new Error('Invalid request type provided by resource provider.')
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async createRequest(
|
|
234
|
+
response: ResourceProviderResponse,
|
|
235
|
+
context: TransactContext
|
|
236
|
+
): Promise<SigningRequest> {
|
|
237
|
+
// Create a new signing request based on the response to return to the session's transact flow.
|
|
238
|
+
const request = await SigningRequest.create(
|
|
239
|
+
{transaction: response.data.request[1]},
|
|
240
|
+
context.esrOptions
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
// Set the required fee onto the request itself for wallets to process.
|
|
244
|
+
if (response.code === 402 && response.data.fee) {
|
|
245
|
+
request.setInfoKey('txfee', Asset.from(response.data.fee))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// If the fee costs exist, set them on the request for the signature provider to consume
|
|
249
|
+
if (response.data.costs) {
|
|
250
|
+
request.setInfoKey('txfeecpu', response.data.costs.cpu)
|
|
251
|
+
request.setInfoKey('txfeenet', response.data.costs.net)
|
|
252
|
+
request.setInfoKey('txfeeram', response.data.costs.ram)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return request
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Perform validation against the request to ensure it is valid for the resource provider.
|
|
259
|
+
*/
|
|
260
|
+
validateRequest(request: SigningRequest, context: TransactContext): void {
|
|
261
|
+
// Retrieve first authorizer and ensure it matches session context.
|
|
262
|
+
const firstAction = request.getRawActions()[0]
|
|
263
|
+
const firstAuthorizer = firstAction.authorization[0]
|
|
264
|
+
if (!firstAuthorizer.actor.equals(context.permissionLevel.actor)) {
|
|
265
|
+
throw new Error('The first authorizer of the transaction does not match this session.')
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Perform validation against the response to ensure it is valid for the session.
|
|
270
|
+
*/
|
|
271
|
+
async validateResponseData(response: Record<string, any>): Promise<void> {
|
|
272
|
+
// If the data wasn't provided in the response, throw an error.
|
|
273
|
+
if (!response) {
|
|
274
|
+
throw new Error('Resource provider did not respond to the request.')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// If a malformed response with a fee was provided, throw an error.
|
|
278
|
+
if (response.code === 402 && !response.data.fee) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
'Resource provider returned a response indicating required payment, but provided no fee amount.'
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// If no signatures were provided, throw an error.
|
|
285
|
+
if (!response.data.signatures || !response.data.signatures[0]) {
|
|
286
|
+
throw new Error('Resource provider did not return a signature.')
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {Action, Transaction} from '@wharfkit/session'
|
|
2
|
+
|
|
3
|
+
export function hasOriginalActions(original: Transaction, modified: Transaction): boolean {
|
|
4
|
+
return original.actions.every((originalAction: Action) => {
|
|
5
|
+
return modified.actions.some((modifiedAction: Action) => {
|
|
6
|
+
// Ensure the original contract account matches
|
|
7
|
+
const matchesOriginalContractAccount = originalAction.account.equals(
|
|
8
|
+
modifiedAction.account
|
|
9
|
+
)
|
|
10
|
+
// Ensure the original contract action matches
|
|
11
|
+
const matchesOriginalContractAction = originalAction.name.equals(modifiedAction.name)
|
|
12
|
+
// Ensure the original authorization is in tact
|
|
13
|
+
const matchesOriginalAuthorization =
|
|
14
|
+
originalAction.authorization.length === modifiedAction.authorization.length &&
|
|
15
|
+
originalAction.authorization[0].actor.equals(modifiedAction.authorization[0].actor)
|
|
16
|
+
// Ensure the original action data matches
|
|
17
|
+
const matchesOriginalActionData = originalAction.data.equals(modifiedAction.data)
|
|
18
|
+
// Return any action that does not match the original
|
|
19
|
+
return (
|
|
20
|
+
matchesOriginalContractAccount &&
|
|
21
|
+
matchesOriginalContractAction &&
|
|
22
|
+
matchesOriginalAuthorization &&
|
|
23
|
+
matchesOriginalActionData
|
|
24
|
+
)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getNewActions(original: Transaction, modified: Transaction): Action[] {
|
|
30
|
+
return modified.actions.filter((modifiedAction: Action) => {
|
|
31
|
+
return original.actions.some((originalAction: Action) => {
|
|
32
|
+
// Ensure the original contract account matches
|
|
33
|
+
const matchesOriginalContractAccount = originalAction.account.equals(
|
|
34
|
+
modifiedAction.account
|
|
35
|
+
)
|
|
36
|
+
// Ensure the original contract action matches
|
|
37
|
+
const matchesOriginalContractAction = originalAction.name.equals(modifiedAction.name)
|
|
38
|
+
// Ensure the original authorization is in tact
|
|
39
|
+
const matchesOriginalAuthorization =
|
|
40
|
+
originalAction.authorization.length === modifiedAction.authorization.length &&
|
|
41
|
+
originalAction.authorization[0].actor.equals(modifiedAction.authorization[0].actor)
|
|
42
|
+
// Ensure the original action data matches
|
|
43
|
+
const matchesOriginalActionData = originalAction.data.equals(modifiedAction.data)
|
|
44
|
+
// Return any action that does not match the original
|
|
45
|
+
return !(
|
|
46
|
+
matchesOriginalContractAccount &&
|
|
47
|
+
matchesOriginalContractAction &&
|
|
48
|
+
matchesOriginalAuthorization &&
|
|
49
|
+
matchesOriginalActionData
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
}
|