11// Copyright 2017-2024 @polkadot/api-contract authors & contributors
22// SPDX-License-Identifier: Apache-2.0
33
4- import type { Bytes } from '@polkadot/types' ;
5- import type { ChainProperties , ContractConstructorSpecLatest , ContractEventSpecLatest , ContractMessageParamSpecLatest , ContractMessageSpecLatest , ContractMetadata , ContractMetadataLatest , ContractProjectInfo , ContractTypeSpec } from '@polkadot/types/interfaces' ;
4+ import type { Bytes , Vec } from '@polkadot/types' ;
5+ import type { ChainProperties , ContractConstructorSpecLatest , ContractEventParamSpecLatest , ContractMessageParamSpecLatest , ContractMessageSpecLatest , ContractMetadata , ContractMetadataV4 , ContractMetadataV5 , ContractProjectInfo , ContractTypeSpec , EventRecord } from '@polkadot/types/interfaces' ;
66import type { Codec , Registry , TypeDef } from '@polkadot/types/types' ;
7- import type { AbiConstructor , AbiEvent , AbiMessage , AbiParam , DecodedEvent , DecodedMessage } from '../types.js' ;
7+ import type { AbiConstructor , AbiEvent , AbiEventParam , AbiMessage , AbiMessageParam , AbiParam , DecodedEvent , DecodedMessage } from '../types.js' ;
88
99import { Option , TypeRegistry } from '@polkadot/types' ;
1010import { TypeDefInfo } from '@polkadot/types-create' ;
1111import { assertReturn , compactAddLength , compactStripLength , isBn , isNumber , isObject , isString , isUndefined , logger , stringCamelCase , stringify , u8aConcat , u8aToHex } from '@polkadot/util' ;
1212
13- import { convertVersions , enumVersions } from './toLatest .js' ;
13+ import { convertVersions , enumVersions } from './toLatestCompatible .js' ;
1414
1515interface AbiJson {
1616 version ?: string ;
1717
1818 [ key : string ] : unknown ;
1919}
2020
21+ type EventOf < M > = M extends { spec : { events : Vec < infer E > } } ? E : never
22+ export type ContractMetadataSupported = ContractMetadataV4 | ContractMetadataV5 ;
23+ type ContractEventSupported = EventOf < ContractMetadataSupported > ;
24+
2125const l = logger ( 'Abi' ) ;
2226
2327const PRIMITIVE_ALWAYS = [ 'AccountId' , 'AccountIndex' , 'Address' , 'Balance' ] ;
@@ -32,7 +36,7 @@ function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string
3236 return assertReturn ( message , ( ) => `Attempted to call an invalid contract interface, ${ stringify ( messageOrId ) } ` ) ;
3337}
3438
35- function getLatestMeta ( registry : Registry , json : AbiJson ) : ContractMetadataLatest {
39+ function getMetadata ( registry : Registry , json : AbiJson ) : ContractMetadataSupported {
3640 // this is for V1, V2, V3
3741 const vx = enumVersions . find ( ( v ) => isObject ( json [ v ] ) ) ;
3842
@@ -50,20 +54,23 @@ function getLatestMeta (registry: Registry, json: AbiJson): ContractMetadataLate
5054 ? { [ `V${ jsonVersion } ` ] : json }
5155 : { V0 : json }
5256 ) ;
57+
5358 const converter = convertVersions . find ( ( [ v ] ) => metadata [ `is${ v } ` ] ) ;
5459
5560 if ( ! converter ) {
56- throw new Error ( `Unable to convert ABI with version ${ metadata . type } to latest ` ) ;
61+ throw new Error ( `Unable to convert ABI with version ${ metadata . type } to a supported version ` ) ;
5762 }
5863
59- return converter [ 1 ] ( registry , metadata [ `as${ converter [ 0 ] } ` ] ) ;
64+ const upgradedMetadata = converter [ 1 ] ( registry , metadata [ `as${ converter [ 0 ] } ` ] ) ;
65+
66+ return upgradedMetadata ;
6067}
6168
62- function parseJson ( json : Record < string , unknown > , chainProperties ?: ChainProperties ) : [ Record < string , unknown > , Registry , ContractMetadataLatest , ContractProjectInfo ] {
69+ function parseJson ( json : Record < string , unknown > , chainProperties ?: ChainProperties ) : [ Record < string , unknown > , Registry , ContractMetadataSupported , ContractProjectInfo ] {
6370 const registry = new TypeRegistry ( ) ;
6471 const info = registry . createType ( 'ContractProjectInfo' , json ) as unknown as ContractProjectInfo ;
65- const latest = getLatestMeta ( registry , json as unknown as AbiJson ) ;
66- const lookup = registry . createType ( 'PortableRegistry' , { types : latest . types } , true ) ;
72+ const metadata = getMetadata ( registry , json as unknown as AbiJson ) ;
73+ const lookup = registry . createType ( 'PortableRegistry' , { types : metadata . types } , true ) ;
6774
6875 // attach the lookup to the registry - now the types are known
6976 registry . setLookup ( lookup ) ;
@@ -77,7 +84,7 @@ function parseJson (json: Record<string, unknown>, chainProperties?: ChainProper
7784 lookup . getTypeDef ( id )
7885 ) ;
7986
80- return [ json , registry , latest , info ] ;
87+ return [ json , registry , metadata , info ] ;
8188}
8289
8390/**
@@ -102,7 +109,7 @@ export class Abi {
102109 readonly info : ContractProjectInfo ;
103110 readonly json : Record < string , unknown > ;
104111 readonly messages : AbiMessage [ ] ;
105- readonly metadata : ContractMetadataLatest ;
112+ readonly metadata : ContractMetadataSupported ;
106113 readonly registry : Registry ;
107114 readonly environment = new Map < string , TypeDef | Codec > ( ) ;
108115
@@ -123,8 +130,8 @@ export class Abi {
123130 : null
124131 } )
125132 ) ;
126- this . events = this . metadata . spec . events . map ( ( spec : ContractEventSpecLatest , index ) =>
127- this . #createEvent( spec , index )
133+ this . events = this . metadata . spec . events . map ( ( _ : ContractEventSupported , index : number ) =>
134+ this . #createEvent( index )
128135 ) ;
129136 this . messages = this . metadata . spec . messages . map ( ( spec : ContractMessageSpecLatest , index ) : AbiMessage =>
130137 this . #createMessage( spec , index , {
@@ -162,7 +169,59 @@ export class Abi {
162169 /**
163170 * Warning: Unstable API, bound to change
164171 */
165- public decodeEvent ( data : Bytes | Uint8Array ) : DecodedEvent {
172+ public decodeEvent ( record : EventRecord ) : DecodedEvent {
173+ switch ( this . metadata . version . toString ( ) ) {
174+ // earlier version are hoisted to v4
175+ case '4' :
176+ return this . #decodeEventV4( record ) ;
177+ // Latest
178+ default :
179+ return this . #decodeEventV5( record ) ;
180+ }
181+ }
182+
183+ #decodeEventV5 = ( record : EventRecord ) : DecodedEvent => {
184+ // Find event by first topic, which potentially is the signature_topic
185+ const signatureTopic = record . topics [ 0 ] ;
186+ const data = record . event . data [ 1 ] as Bytes ;
187+
188+ if ( signatureTopic ) {
189+ const event = this . events . find ( ( e ) => e . signatureTopic !== undefined && e . signatureTopic !== null && e . signatureTopic === signatureTopic . toHex ( ) ) ;
190+
191+ // Early return if event found by signature topic
192+ if ( event ) {
193+ return event . fromU8a ( data ) ;
194+ }
195+ }
196+
197+ // If no event returned yet, it might be anonymous
198+ const amountOfTopics = record . topics . length ;
199+ const potentialEvents = this . events . filter ( ( e ) => {
200+ // event can't have a signature topic
201+ if ( e . signatureTopic !== null && e . signatureTopic !== undefined ) {
202+ return false ;
203+ }
204+
205+ // event should have same amount of indexed fields as emitted topics
206+ const amountIndexed = e . args . filter ( ( a ) => a . indexed ) . length ;
207+
208+ if ( amountIndexed !== amountOfTopics ) {
209+ return false ;
210+ }
211+
212+ // If all conditions met, it's a potential event
213+ return true ;
214+ } ) ;
215+
216+ if ( potentialEvents . length === 1 ) {
217+ return potentialEvents [ 0 ] . fromU8a ( data ) ;
218+ }
219+
220+ throw new Error ( 'Unable to determine event' ) ;
221+ } ;
222+
223+ #decodeEventV4 = ( record : EventRecord ) : DecodedEvent => {
224+ const data = record . event . data [ 1 ] as Bytes ;
166225 const index = data [ 0 ] ;
167226 const event = this . events [ index ] ;
168227
@@ -171,7 +230,7 @@ export class Abi {
171230 }
172231
173232 return event . fromU8a ( data . subarray ( 1 ) ) ;
174- }
233+ } ;
175234
176235 /**
177236 * Warning: Unstable API, bound to change
@@ -195,7 +254,7 @@ export class Abi {
195254 return findMessage ( this . messages , messageOrId ) ;
196255 }
197256
198- #createArgs = ( args : ContractMessageParamSpecLatest [ ] , spec : unknown ) : AbiParam [ ] => {
257+ #createArgs = ( args : ContractMessageParamSpecLatest [ ] | ContractEventParamSpecLatest [ ] , spec : unknown ) : AbiParam [ ] => {
199258 return args . map ( ( { label, type } , index ) : AbiParam => {
200259 try {
201260 if ( ! isObject ( type ) ) {
@@ -233,8 +292,47 @@ export class Abi {
233292 } ) ;
234293 } ;
235294
236- #createEvent = ( spec : ContractEventSpecLatest , index : number ) : AbiEvent => {
237- const args = this . #createArgs( spec . args , spec ) ;
295+ #createMessageParams = ( args : ContractMessageParamSpecLatest [ ] , spec : unknown ) : AbiMessageParam [ ] => {
296+ return this . #createArgs( args , spec ) ;
297+ } ;
298+
299+ #createEventParams = ( args : ContractEventParamSpecLatest [ ] , spec : unknown ) : AbiEventParam [ ] => {
300+ const params = this . #createArgs( args , spec ) ;
301+
302+ return params . map ( ( p , index ) : AbiEventParam => ( { ...p , indexed : args [ index ] . indexed . toPrimitive ( ) } ) ) ;
303+ } ;
304+
305+ #createEvent = ( index : number ) : AbiEvent => {
306+ // TODO TypeScript would narrow this type to the correct version,
307+ // but version is `Text` so I need to call `toString()` here,
308+ // which breaks the type inference.
309+ switch ( this . metadata . version . toString ( ) ) {
310+ case '4' :
311+ return this . #createEventV4( ( this . metadata as ContractMetadataV4 ) . spec . events [ index ] , index ) ;
312+ default :
313+ return this . #createEventV5( ( this . metadata as ContractMetadataV5 ) . spec . events [ index ] , index ) ;
314+ }
315+ } ;
316+
317+ #createEventV5 = ( spec : EventOf < ContractMetadataV5 > , index : number ) : AbiEvent => {
318+ const args = this . #createEventParams( spec . args , spec ) ;
319+ const event = {
320+ args,
321+ docs : spec . docs . map ( ( d ) => d . toString ( ) ) ,
322+ fromU8a : ( data : Uint8Array ) : DecodedEvent => ( {
323+ args : this . #decodeArgs( args , data ) ,
324+ event
325+ } ) ,
326+ identifier : [ spec . module_path , spec . label ] . join ( '::' ) ,
327+ index,
328+ signatureTopic : spec . signature_topic . isSome ? spec . signature_topic . unwrap ( ) . toHex ( ) : null
329+ } ;
330+
331+ return event ;
332+ } ;
333+
334+ #createEventV4 = ( spec : EventOf < ContractMetadataV4 > , index : number ) : AbiEvent => {
335+ const args = this . #createEventParams( spec . args , spec ) ;
238336 const event = {
239337 args,
240338 docs : spec . docs . map ( ( d ) => d . toString ( ) ) ,
@@ -250,7 +348,7 @@ export class Abi {
250348 } ;
251349
252350 #createMessage = ( spec : ContractMessageSpecLatest | ContractConstructorSpecLatest , index : number , add : Partial < AbiMessage > = { } ) : AbiMessage => {
253- const args = this . #createArgs ( spec . args , spec ) ;
351+ const args = this . #createMessageParams ( spec . args , spec ) ;
254352 const identifier = spec . label . toString ( ) ;
255353 const message = {
256354 ...add ,
@@ -267,7 +365,7 @@ export class Abi {
267365 path : identifier . split ( '::' ) . map ( ( s ) => stringCamelCase ( s ) ) ,
268366 selector : spec . selector ,
269367 toU8a : ( params : unknown [ ] ) =>
270- this . #encodeArgs ( spec , args , params )
368+ this . #encodeMessageArgs ( spec , args , params )
271369 } ;
272370
273371 return message ;
@@ -299,7 +397,7 @@ export class Abi {
299397 return message . fromU8a ( trimmed . subarray ( 4 ) ) ;
300398 } ;
301399
302- #encodeArgs = ( { label, selector } : ContractMessageSpecLatest | ContractConstructorSpecLatest , args : AbiParam [ ] , data : unknown [ ] ) : Uint8Array => {
400+ #encodeMessageArgs = ( { label, selector } : ContractMessageSpecLatest | ContractConstructorSpecLatest , args : AbiMessageParam [ ] , data : unknown [ ] ) : Uint8Array => {
303401 if ( data . length !== args . length ) {
304402 throw new Error ( `Expected ${ args . length } arguments to contract message '${ label . toString ( ) } ', found ${ data . length } ` ) ;
305403 }
0 commit comments