diff --git a/src/serializer/decoder.ts b/src/serializer/decoder.ts index a8b0bc2f..aaa284be 100644 --- a/src/serializer/decoder.ts +++ b/src/serializer/decoder.ts @@ -150,6 +150,20 @@ interface DecodingContext { /** Marker for objects when they have been resolved, i.e. their types `from` factory method will not need to resolve children. */ export const Resolved = Symbol('Resolved') +/** True if this type, or an alias it resolves to, is optional. */ +function resolvesOptional(type: ABI.ResolvedType): boolean { + let current: ABI.ResolvedType | undefined = type + const seen = new Set() + while (current && !seen.has(current)) { + if (current.isOptional) { + return true + } + seen.add(current) + current = current.ref + } + return false +} + function decodeBinary(type: ABI.ResolvedType, decoder: ABIDecoder, ctx: DecodingContext): any { if (ctx.codingPath.length > 32) { throw new Error('Maximum decoding depth exceeded') @@ -199,7 +213,10 @@ function decodeBinary(type: ABI.ResolvedType, decoder: ABIDecoder, ctx: Decoding const rv: any = {} for (const field of fields) { ctx.codingPath.push({field: field.name, type: field.type}) - rv[field.name] = decodeBinary(field.type, decoder, ctx) + const value = decodeBinary(field.type, decoder, ctx) + if (!(value === null && resolvesOptional(field.type))) { + rv[field.name] = value + } ctx.codingPath.pop() } if (abiType) { @@ -284,7 +301,10 @@ function decodeObject(value: any, type: ABI.ResolvedType, ctx: DecodingContext): const struct: any = {} for (const field of fields) { ctx.codingPath.push({field: field.name, type: field.type}) - struct[field.name] = decodeObject(value[field.name], field.type, ctx) + const fieldValue = decodeObject(value[field.name], field.type, ctx) + if (!(fieldValue === null && resolvesOptional(field.type))) { + struct[field.name] = fieldValue + } ctx.codingPath.pop() } if (abiType) { @@ -355,7 +375,10 @@ function defaultValue( const rv: any = {} for (const field of type.allFields) { ctx.codingPath.push({field: field.name, type: field.type}) - rv[field.name] = defaultValue(field.type, ctx, seen) + const value = defaultValue(field.type, ctx, seen) + if (!(value === null && resolvesOptional(field.type))) { + rv[field.name] = value + } ctx.codingPath.pop() } if (abiType) { diff --git a/src/serializer/index.ts b/src/serializer/index.ts index a930e23e..cdc0beda 100644 --- a/src/serializer/index.ts +++ b/src/serializer/index.ts @@ -43,7 +43,11 @@ export namespace Serializer { } const rv: any = {} for (const key of Object.keys(v)) { - rv[key] = walk(v[key]) + const value = walk(v[key]) + if (value === null || value === undefined) { + continue + } + rv[key] = value } return rv } diff --git a/test/serializer.ts b/test/serializer.ts index 0f78e824..621a4199 100644 --- a/test/serializer.ts +++ b/test/serializer.ts @@ -847,8 +847,13 @@ suite('serializer', function () { 'c3dfdd231ed38907504900000000005049000000000000765edf01000000000750490000000000765edf01000000000750' + '49000000000000000053419a81ab0101010001020101000568656c6c6f0105776f726c6400' ) - const decoded = Serializer.decode({data, type: 'all_types', abi}) - assert.deepStrictEqual(JSON.parse(JSON.stringify(decoded)), object) + const decoded = Serializer.decode({data, type: 'all_types', abi}) as any + const expected = JSON.parse(JSON.stringify(object)) + delete expected.alias6 + delete expected.extension.extension.extension + assert.deepStrictEqual(JSON.parse(JSON.stringify(decoded)), expected) + assert.notProperty(decoded, 'alias6') + assert.notProperty(decoded.extension.extension, 'extension') }) test('coder metadata', function () { @@ -1222,6 +1227,59 @@ suite('serializer', function () { assert.notProperty(Serializer.objectify(res1), 'proposal_hash') }) + test('ABI decode omits absent optional fields', function () { + const abi = ABI.from({ + structs: [ + { + name: 'permission_level', + base: '', + fields: [ + {name: 'actor', type: 'name'}, + {name: 'permission', type: 'name'}, + ], + }, + { + name: 'approve', + base: '', + fields: [ + {name: 'proposer', type: 'name'}, + {name: 'proposal_name', type: 'name'}, + {name: 'level', type: 'permission_level'}, + {name: 'proposal_hash', type: 'checksum256?'}, + ], + }, + ], + actions: [{name: 'approve', type: 'approve', ricardian_contract: ''}], + }) + const object = { + proposer: 'foo', + proposal_name: 'bar', + level: { + actor: 'baz', + permission: 'active', + }, + } + const data = Serializer.encode({object, abi, type: 'approve'}) + const decoded = Serializer.decode({data, abi, type: 'approve'}) + const objectified = Serializer.objectify(decoded) + assert.notProperty(decoded, 'proposal_hash') + assert.notProperty(objectified, 'proposal_hash') + assert.deepStrictEqual(objectified, object) + + const fromObject = Serializer.decode({object, abi, type: 'approve'}) + assert.notProperty(fromObject, 'proposal_hash') + assert.notProperty(Serializer.objectify(fromObject), 'proposal_hash') + + const hash = '00'.repeat(32) + const withHash = {...object, proposal_hash: hash} + const decodedWithHash = Serializer.decode({ + data: Serializer.encode({object: withHash, abi, type: 'approve'}), + abi, + type: 'approve', + }) + assert.equal(Serializer.objectify(decodedWithHash).proposal_hash, hash) + }) + test('action_results', function () { const raw = { ____comment: 'This file was generated with eosio-abigen. DO NOT EDIT ',