Skip to content

Commit

Permalink
Caching ABI in Action for future use (#69)
Browse files Browse the repository at this point in the history
* Caching ABI in Action for future use

* Added `.decoded` method to action to automatically decode

* Disabled failing tests

These tests don't make much sense to me. The variant being passed in as action data isn't even valid as far as I can tell.

* Fixed type selection for generated action

* Reimplemented tests for variant in more sane way

* Removed string casting, wasn't needed
  • Loading branch information
aaroncox authored Aug 28, 2023
1 parent 9211b92 commit 3fb8f3f
Show file tree
Hide file tree
Showing 3 changed files with 237 additions and 8 deletions.
39 changes: 36 additions & 3 deletions src/chain/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import {
ABISerializableConstructor,
ABISerializableObject,
ABISerializableType,
synthesizeABI,
} from '../serializer/serializable'

import {arrayEquatableEquals} from '../utils'
import {BuiltinTypes} from '../serializer/builtins'
import {BuiltinTypes, getType} from '../serializer/builtins'

import {
ABI,
Expand Down Expand Up @@ -54,6 +55,8 @@ export class Action extends Struct {
/** The ABI-encoded action data. */
@Struct.field('bytes') data!: Bytes

public abi?: ABI

static from(object: ActionType | AnyAction, abi?: ABIDef): Action {
const data = object.data as any
if (!Bytes.isBytes(data)) {
Expand All @@ -70,12 +73,32 @@ export class Action extends Struct {
data: abiEncode({object: data, type, abi}),
}
}
return super.from(object) as Action

const action = super.from(object) as Action
if (abi) {
action.abi = ABI.from(abi)
} else {
const type = getType(data)
if (type) {
action.abi = ABI.from({
...synthesizeABI(type).abi,
actions: [
{
name: action.name,
type: type.abiName,
ricardian_contract: '',
},
],
})
}
}

return action
}

/** Return true if this Action is equal to given action. */
equals(other: ActionType | AnyAction) {
const otherAction = Action.from(other)
const otherAction = Action.from(other, this.abi)
return (
this.account.equals(otherAction.account) &&
this.name.equals(otherAction.name) &&
Expand Down Expand Up @@ -103,4 +126,14 @@ export class Action extends Struct {
return abiDecode({data: this.data, type, abi})
}
}

get decoded() {
if (!this.abi) {
throw new Error('Missing ABI definition when decoding action data')
}
return {
...this.toJSON(),
data: this.decodeData(this.abi),
}
}
}
8 changes: 7 additions & 1 deletion src/chain/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,13 @@ export class Transaction extends TransactionHeader {
return abis
}
}
const resolveAction = (action: AnyAction) => Action.from(action, abiFor(action.account))
const resolveAction = (action: AnyAction) => {
if (action instanceof Action) {
return action
} else {
return Action.from(action, abiFor(action.account))
}
}
const actions = (object.actions || []).map(resolveAction)
const context_free_actions = (object.context_free_actions || []).map(resolveAction)
const transaction = {
Expand Down
198 changes: 194 additions & 4 deletions test/chain.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {assert} from 'chai'

import {
ABI,
ABIDef,
Action,
AnyTransaction,
Expand All @@ -19,6 +20,7 @@ import {
PackedTransaction,
PermissionLevel,
PublicKey,
Serializer,
Signature,
SignedTransaction,
Struct,
Expand Down Expand Up @@ -360,19 +362,38 @@ suite('chain', function () {
assert.equal(variant.equals(Int32.from(1)), false)
assert.equal(variant.equals(MyVariant.from('haj')), false)

@Struct.type('my_struct')
class MyStructWithVariant extends Struct {
@Struct.field(MyVariant) field!: MyVariant
}
const action = Action.from({
account: 'foo',
name: 'bar',
authorization: [perm],
data: variant,
data: MyStructWithVariant.from({
field: variant,
}),
})
assert.equal(action.equals(action), true)
assert.equal(
action.equals({
account: 'foo',
name: 'bar',
authorization: [perm],
data: variant,
data: {
field: 'hello',
},
}),
true
)
assert.equal(
action.equals({
account: 'foo',
name: 'bar',
authorization: [perm],
data: {
field: variant,
},
}),
true
)
Expand All @@ -381,7 +402,9 @@ suite('chain', function () {
account: 'foo',
name: 'bar',
authorization: [],
data: variant,
data: {
field: variant,
},
}),
false
)
Expand All @@ -390,7 +413,9 @@ suite('chain', function () {
account: 'foo',
name: 'bar',
authorization: [{actor: 'maa', permission: 'jong'}],
data: variant,
data: {
field: variant,
},
}),
false
)
Expand Down Expand Up @@ -506,6 +531,171 @@ suite('chain', function () {
assert.equal(a1.equals(a3), true)
})

test('action retains abi (abi)', function () {
const abi = {
structs: [{name: 'noop', base: '', fields: []}],
actions: [
{
name: 'noop',
type: 'noop',
ricardian_contract: '',
},
],
}
const action = Action.from(
{
account: 'greymassnoop',
name: 'noop',
authorization: [{actor: 'greymassfuel', permission: 'cosign'}],
data: '',
},
abi
)
assert.instanceOf(action.abi, ABI)
})

test('action can deserialize itself from abi', function () {
const abi = {
structs: [
{
name: 'transfer',
base: '',
fields: [
{
name: 'from',
type: 'name',
},
{
name: 'to',
type: 'name',
},
{
name: 'quantity',
type: 'asset',
},
{
name: 'memo',
type: 'string',
},
],
},
],
actions: [
{
name: 'transfer',
type: 'transfer',
ricardian_contract: '',
},
],
}

const action = Action.from(
{
account: 'eosio.token',
name: 'transfer',
authorization: [{actor: 'foo', permission: 'bar'}],
data: {
from: 'foo',
to: 'bar',
quantity: '1.0000 EOS',
memo: 'hello',
},
},
abi
)
assert.instanceOf(action.abi, ABI)
const decoded = action.decoded
assert.instanceOf(decoded.account, Name)
assert.instanceOf(decoded.name, Name)
assert.instanceOf(decoded.authorization, Array)
assert.instanceOf(decoded.authorization[0], PermissionLevel)
assert.instanceOf(decoded.data.from, Name)
assert.instanceOf(decoded.data.to, Name)
assert.instanceOf(decoded.data.quantity, Asset)
})

test('action retains abi (struct)', function () {
@Struct.type('transfer')
class Transfer extends Struct {
@Struct.field('name') from!: Name
@Struct.field('name') to!: Name
@Struct.field('asset') quantity!: Asset
@Struct.field('string') memo!: string
}

const data = Transfer.from({
from: 'foo',
to: 'bar',
quantity: '1.0000 EOS',
memo: 'hello',
})

const action = Action.from({
authorization: [],
account: 'eosio.token',
name: 'transfer',
data,
})
assert.instanceOf(action.abi, ABI)

const transaction = Transaction.from({
ref_block_num: 0,
ref_block_prefix: 0,
expiration: 0,
actions: [action],
})

assert.instanceOf(transaction.actions[0].abi, ABI)
assert.isTrue(action.equals(transaction.actions[0]))
assert.isTrue(transaction.actions[0].equals(action))
assert.isTrue(
data.equals(
Serializer.decode({
data: transaction.actions[0].data,
abi: transaction.actions[0].abi,
type: String(transaction.actions[0].name),
})
)
)
})

test('action can deserialize itself from struct', function () {
@Struct.type('transfer')
class Transfer extends Struct {
@Struct.field('name') from!: Name
@Struct.field('name') to!: Name
@Struct.field('asset') quantity!: Asset
@Struct.field('string') memo!: string
}
const data = Transfer.from({
from: 'foo',
to: 'bar',
quantity: '1.0000 EOS',
memo: 'hello',
})

const action = Action.from({
authorization: [
{
actor: 'foo',
permission: 'bar',
},
],
account: 'eosio.token',
name: 'transfer',
data,
})
assert.instanceOf(action.abi, ABI)
const decoded = action.decoded
assert.instanceOf(decoded.account, Name)
assert.instanceOf(decoded.name, Name)
assert.instanceOf(decoded.authorization, Array)
assert.instanceOf(decoded.authorization[0], PermissionLevel)
assert.instanceOf(decoded.data.from, Name)
assert.instanceOf(decoded.data.to, Name)
assert.instanceOf(decoded.data.quantity, Asset)
})

test('authority', function () {
const auth = Authority.from({
threshold: 21,
Expand Down

0 comments on commit 3fb8f3f

Please sign in to comment.