Skip to content

Commit b35dfa9

Browse files
authored
refactor: more flexible extrinsic unwrapping for ctype fetching (#837)
* refactor!: flattenCalls unpacks all calls * test: add integration tests for dispatchAs * fix: add conditional logic to event & extrinsic matching * docs: update docstring on flattenCalls
1 parent a2def69 commit b35dfa9

File tree

8 files changed

+200
-156
lines changed

8 files changed

+200
-156
lines changed

packages/asset-credentials/src/credentials/PublicCredential.chain.ts

+31-68
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ import type {
1414
Did,
1515
HexString,
1616
} from '@kiltprotocol/types'
17-
import type { ApiPromise } from '@polkadot/api'
1817
import type { GenericCall, Option } from '@polkadot/types'
19-
import type { Call } from '@polkadot/types/interfaces'
2018
import type { BN } from '@polkadot/util'
2119
import type {
2220
PublicCredentialsCredentialsCredential,
@@ -125,28 +123,6 @@ export function fromChain(
125123
}
126124

127125
// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.publicCredentials.add`.
128-
function extractPublicCredentialCreationCallsFromDidCall(
129-
api: ApiPromise,
130-
call: Call
131-
): Array<GenericCall<typeof api.tx.publicCredentials.add.args>> {
132-
const extrinsicCalls = Blockchain.flattenCalls(call, api)
133-
return extrinsicCalls.filter(
134-
(c): c is GenericCall<typeof api.tx.publicCredentials.add.args> =>
135-
api.tx.publicCredentials.add.is(c)
136-
)
137-
}
138-
139-
// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.did.submitDidCall`.
140-
function extractDidCallsFromBatchCall(
141-
api: ApiPromise,
142-
call: Call
143-
): Array<GenericCall<typeof api.tx.did.submitDidCall.args>> {
144-
const extrinsicCalls = Blockchain.flattenCalls(call, api)
145-
return extrinsicCalls.filter(
146-
(c): c is GenericCall<typeof api.tx.did.submitDidCall.args> =>
147-
api.tx.did.submitDidCall.is(c)
148-
)
149-
}
150126

151127
/**
152128
* Retrieves from the blockchain the {@link IPublicCredential} that is identified by the provided identifier.
@@ -164,14 +140,19 @@ export async function fetchCredentialFromChain(
164140
const publicCredentialEntry = await api.call.publicCredentials.getById(
165141
credentialId
166142
)
167-
const { blockNumber, revoked } = publicCredentialEntry.unwrap()
143+
const {
144+
blockNumber,
145+
revoked,
146+
attester: attesterId,
147+
} = publicCredentialEntry.unwrap()
148+
const attester = didFromChain(attesterId)
168149

169150
const extrinsic = await Blockchain.retrieveExtrinsicFromBlock(
170151
blockNumber,
171152
({ events }) =>
172153
events.some(
173154
(event) =>
174-
api.events.publicCredentials.CredentialStored.is(event) &&
155+
api.events.publicCredentials?.CredentialStored?.is(event) &&
175156
event.data[1].toString() === credentialId
176157
),
177158
api
@@ -183,58 +164,40 @@ export async function fetchCredentialFromChain(
183164
)
184165
}
185166

186-
if (
187-
!Blockchain.isBatch(extrinsic, api) &&
188-
!api.tx.did.submitDidCall.is(extrinsic)
189-
) {
190-
throw new SDKErrors.PublicCredentialError(
191-
'Extrinsic should be either a `did.submitDidCall` extrinsic or a batch with at least a `did.submitDidCall` extrinsic'
192-
)
193-
}
194-
195-
// If we're dealing with a batch, flatten any nested `submit_did_call` calls,
196-
// otherwise the extrinsic is itself a submit_did_call, so just take it.
197-
const didCalls = Blockchain.isBatch(extrinsic, api)
198-
? extrinsic.args[0].flatMap((batchCall) =>
199-
extractDidCallsFromBatchCall(api, batchCall)
200-
)
201-
: [extrinsic]
202-
203-
// From the list of DID calls, only consider public_credentials::add calls, bundling each of them with their DID submitter.
204-
// It returns a list of [reconstructedCredential, attesterDid].
205-
const callCredentialsContent = didCalls.flatMap((didCall) => {
206-
const publicCredentialCalls =
207-
extractPublicCredentialCreationCallsFromDidCall(api, didCall.args[0].call)
208-
// Re-create the issued public credential for each call identified.
209-
return publicCredentialCalls.map(
210-
(credentialCreationCall) =>
211-
[
212-
credentialInputFromChain(credentialCreationCall.args[0]),
213-
didFromChain(didCall.args[0].did),
214-
] as const
215-
)
216-
})
167+
// Unpack any nested calls, e.g., within a batch or `submit_did_call`
168+
const extrinsicCalls = Blockchain.flattenCalls(extrinsic, api)
217169

218-
// If more than one call is present, it always considers the last one as the valid one, and takes its attester.
219-
const lastRightCredentialCreationCall = callCredentialsContent
220-
.reverse()
221-
.find(([credential, attester]) => {
222-
const reconstructedId = getIdForCredential(credential, attester)
223-
return reconstructedId === credentialId
224-
})
170+
// only consider public_credentials::add calls
171+
const publicCredentialCalls = extrinsicCalls.filter(
172+
(c): c is GenericCall<typeof api.tx.publicCredentials.add.args> =>
173+
api.tx.publicCredentials?.add?.is(c)
174+
)
225175

226-
if (!lastRightCredentialCreationCall) {
176+
// Re-create the issued public credential for each call identified to find the credential with the id we're looking for
177+
const credentialInput = publicCredentialCalls.reduceRight<
178+
IPublicCredentialInput | undefined
179+
>((selectedCredential, credentialCreationCall) => {
180+
if (selectedCredential) {
181+
return selectedCredential
182+
}
183+
const credential = credentialInputFromChain(credentialCreationCall.args[0])
184+
const reconstructedId = getIdForCredential(credential, attester)
185+
if (reconstructedId === credentialId) {
186+
return credential
187+
}
188+
return undefined
189+
}, undefined)
190+
191+
if (typeof credentialInput === 'undefined') {
227192
throw new SDKErrors.PublicCredentialError(
228193
'Block should always contain the full credential, eventually.'
229194
)
230195
}
231196

232-
const [credentialInput, attester] = lastRightCredentialCreationCall
233-
234197
return {
235198
...credentialInput,
236199
attester,
237-
id: getIdForCredential(credentialInput, attester),
200+
id: credentialId,
238201
blockNumber,
239202
revoked: revoked.toPrimitive(),
240203
}

packages/chain-helpers/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"typescript": "^4.8.3"
3535
},
3636
"dependencies": {
37+
"@kiltprotocol/augment-api": "workspace:^",
3738
"@kiltprotocol/config": "workspace:*",
3839
"@kiltprotocol/type-definitions": "workspace:*",
3940
"@kiltprotocol/types": "workspace:*",

packages/chain-helpers/src/blockchain/Blockchain.ts

+32-11
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
* found in the LICENSE file in the root directory of this source tree.
66
*/
77

8+
import '@kiltprotocol/augment-api'
9+
810
import type { ApiPromise } from '@polkadot/api'
911
import type { TxWithEvent } from '@polkadot/api-derive/types'
10-
import type { GenericCall, GenericExtrinsic, Vec } from '@polkadot/types'
12+
import type { Vec } from '@polkadot/types'
1113
import type { Call, Extrinsic } from '@polkadot/types/interfaces'
12-
import type { AnyNumber } from '@polkadot/types/types'
14+
import type { AnyNumber, IMethod } from '@polkadot/types/types'
1315
import type { BN } from '@polkadot/util'
1416

1517
import type {
@@ -210,31 +212,50 @@ export async function signAndSubmitTx(
210212
* @returns True if it's a batch, false otherwise.
211213
*/
212214
export function isBatch(
213-
extrinsic: Extrinsic | Call,
215+
extrinsic: IMethod,
214216
api?: ApiPromise
215-
): extrinsic is GenericExtrinsic<[Vec<Call>]> | GenericCall<[Vec<Call>]> {
217+
): extrinsic is IMethod<[Vec<Call>]> {
216218
const apiPromise = api ?? ConfigService.get('api')
217219
return (
218-
apiPromise.tx.utility.batch.is(extrinsic) ||
219-
apiPromise.tx.utility.batchAll.is(extrinsic) ||
220-
apiPromise.tx.utility.forceBatch.is(extrinsic)
220+
apiPromise.tx.utility?.batch?.is(extrinsic) ||
221+
apiPromise.tx.utility?.batchAll?.is(extrinsic) ||
222+
apiPromise.tx.utility?.forceBatch?.is(extrinsic)
221223
)
222224
}
223225

224226
/**
225-
* Flatten all calls into a single array following a DFS approach.
227+
* Flatten all nested calls into a single array following a DFS approach.
226228
*
227229
* For example, given the calls [[N1, N2], [N3, [N4, N5], N6]], the final list will look like [N1, N2, N3, N4, N5, N6].
228230
*
231+
* The following extrinsics are recognized as containing nested calls and will be unpacked:
232+
*
233+
* - pallet `utility`: `batch`, `batchAll`, `forceBatch`.
234+
* - pallet `did`: `submitDidCall`, `dispatchAs`.
235+
* - pallet `proxy`: `proxy`, `proxyAnnounced`.
236+
*
229237
* @param call The {@link Call} which can potentially contain nested calls.
230238
* @param api The optional {@link ApiPromise}. If not provided, the one returned by the `ConfigService` is used.
231239
*
232240
* @returns A list of {@link Call} nested according to the rules above.
233241
*/
234-
export function flattenCalls(call: Call, api?: ApiPromise): Call[] {
235-
if (isBatch(call, api)) {
242+
export function flattenCalls(call: IMethod, api?: ApiPromise): IMethod[] {
243+
const apiObject = api ?? ConfigService.get('api')
244+
if (isBatch(call, apiObject)) {
236245
// Inductive case
237-
return call.args[0].flatMap((c) => flattenCalls(c, api))
246+
return call.args[0].flatMap((c) => flattenCalls(c, apiObject))
247+
}
248+
if (apiObject.tx.did?.submitDidCall?.is(call)) {
249+
return flattenCalls(call.args[0].call, apiObject)
250+
}
251+
if (apiObject.tx.did?.dispatchAs?.is(call)) {
252+
return flattenCalls(call.args[1], apiObject)
253+
}
254+
if (apiObject.tx.proxy?.proxy?.is(call)) {
255+
return flattenCalls(call.args[2], apiObject)
256+
}
257+
if (apiObject.tx.proxy?.proxyAnnounced?.is(call)) {
258+
return flattenCalls(call.args[3], apiObject)
238259
}
239260
// Base case
240261
return [call]

packages/credentials/src/V1/KiltAttestationProofV1.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ async function verifyAttestedAt(
244244
.find(
245245
({ phase, event }) =>
246246
phase.isApplyExtrinsic &&
247-
api.events.attestation.AttestationCreated.is(event) &&
247+
api.events.attestation?.AttestationCreated?.is(event) &&
248248
u8aEq(event.data[1], claimHash)
249249
)
250250
if (!attestationEvent)

packages/credentials/src/ctype/CType.chain.ts

+30-75
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@
55
* found in the LICENSE file in the root directory of this source tree.
66
*/
77

8-
import type { ApiPromise } from '@polkadot/api'
98
import type { Bytes, GenericCall, Option } from '@polkadot/types'
10-
import type { AccountId, Call } from '@polkadot/types/interfaces'
9+
import type { AccountId } from '@polkadot/types/interfaces'
1110
import type { BN } from '@polkadot/util'
1211

1312
import type { CtypeCtypeEntry } from '@kiltprotocol/augment-api'
@@ -118,30 +117,6 @@ export function fromChain(
118117
}
119118
}
120119

121-
// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.ctype.add`.
122-
function extractCTypeCreationCallsFromDidCall(
123-
api: ApiPromise,
124-
call: Call
125-
): Array<GenericCall<typeof api.tx.ctype.add.args>> {
126-
const extrinsicCalls = Blockchain.flattenCalls(call, api)
127-
return extrinsicCalls.filter(
128-
(c): c is GenericCall<typeof api.tx.ctype.add.args> =>
129-
api.tx.ctype.add.is(c)
130-
)
131-
}
132-
133-
// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.did.submitDidCall`.
134-
function extractDidCallsFromBatchCall(
135-
api: ApiPromise,
136-
call: Call
137-
): Array<GenericCall<typeof api.tx.did.submitDidCall.args>> {
138-
const extrinsicCalls = Blockchain.flattenCalls(call, api)
139-
return extrinsicCalls.filter(
140-
(c): c is GenericCall<typeof api.tx.did.submitDidCall.args> =>
141-
api.tx.did.submitDidCall.is(c)
142-
)
143-
}
144-
145120
/**
146121
* Resolves a CType identifier to the CType definition by fetching data from the block containing the transaction that registered the CType on chain.
147122
*
@@ -156,7 +131,7 @@ export async function fetchFromChain(
156131
const cTypeHash = idToHash(cTypeId)
157132

158133
const cTypeEntry = await api.query.ctype.ctypes(cTypeHash)
159-
const { createdAt } = fromChain(cTypeEntry)
134+
const { createdAt, creator } = fromChain(cTypeEntry)
160135
if (typeof createdAt === 'undefined')
161136
throw new SDKErrors.CTypeError(
162137
'Cannot fetch CType definitions on a chain that does not store the createdAt block'
@@ -167,72 +142,52 @@ export async function fetchFromChain(
167142
({ events }) =>
168143
events.some(
169144
(event) =>
170-
api.events.ctype.CTypeCreated.is(event) &&
171-
event.data[1].toString() === cTypeHash
145+
api.events.ctype?.CTypeCreated?.is(event) &&
146+
event.data[1].toHex() === cTypeHash
172147
),
173148
api
174149
)
175150

176151
if (extrinsic === null) {
177152
throw new SDKErrors.CTypeError(
178-
`There is not CType with the provided ID "${cTypeId}" on chain.`
153+
`There is no CType with the provided ID "${cTypeId}" on chain.`
179154
)
180155
}
181156

182-
if (
183-
!Blockchain.isBatch(extrinsic, api) &&
184-
!api.tx.did.submitDidCall.is(extrinsic)
185-
) {
186-
throw new SDKErrors.PublicCredentialError(
187-
'Extrinsic should be either a `did.submitDidCall` extrinsic or a batch with at least a `did.submitDidCall` extrinsic'
188-
)
189-
}
157+
// Unpack any nested calls, e.g., within a batch or `submit_did_call`
158+
const extrinsicCalls = Blockchain.flattenCalls(extrinsic, api)
190159

191-
// If we're dealing with a batch, flatten any nested `submit_did_call` calls,
192-
// otherwise the extrinsic is itself a submit_did_call, so just take it.
193-
const didCalls = Blockchain.isBatch(extrinsic, api)
194-
? extrinsic.args[0].flatMap((batchCall) =>
195-
extractDidCallsFromBatchCall(api, batchCall)
196-
)
197-
: [extrinsic]
198-
199-
// From the list of DID calls, only consider ctype::add calls, bundling each of them with their DID submitter.
200-
// It returns a list of [reconstructedCType, attesterDid].
201-
const ctypeCallContent = didCalls.flatMap((didCall) => {
202-
const ctypeCreationCalls = extractCTypeCreationCallsFromDidCall(
203-
api,
204-
didCall.args[0].call
205-
)
206-
// Re-create the issued public credential for each call identified.
207-
return ctypeCreationCalls.map(
208-
(ctypeCreationCall) =>
209-
[
210-
cTypeInputFromChain(ctypeCreationCall.args[0]),
211-
Did.fromChain(didCall.args[0].did),
212-
] as const
213-
)
214-
})
160+
// only consider ctype::add calls
161+
const ctypeCreationCalls = extrinsicCalls.filter(
162+
(c): c is GenericCall<typeof api.tx.ctype.add.args> =>
163+
api.tx.ctype?.add?.is(c)
164+
)
215165

216-
// If more than a call is present, it always considers the last one as the valid one.
217-
const lastRightCTypeCreationCall = ctypeCallContent
218-
.reverse()
219-
.find((cTypeInput) => {
220-
return cTypeInput[0].$id === cTypeId
221-
})
166+
// Re-create the ctype for each call identified to find the right ctype.
167+
// If more than one matching call is present, it always considers the last one as the valid one.
168+
const cTypeDefinition = ctypeCreationCalls.reduceRight<ICType | undefined>(
169+
(selectedCType, cTypeCreationCall) => {
170+
if (selectedCType) {
171+
return selectedCType
172+
}
173+
const cType = cTypeInputFromChain(cTypeCreationCall.args[0])
174+
175+
if (cType.$id === cTypeId) {
176+
return cType
177+
}
178+
return undefined
179+
},
180+
undefined
181+
)
222182

223-
if (!lastRightCTypeCreationCall) {
183+
if (typeof cTypeDefinition === 'undefined') {
224184
throw new SDKErrors.CTypeError(
225185
'Block should always contain the full CType, eventually.'
226186
)
227187
}
228188

229-
const [ctypeInput, creator] = lastRightCTypeCreationCall
230-
231189
return {
232-
cType: {
233-
...ctypeInput,
234-
$id: cTypeId,
235-
},
190+
cType: cTypeDefinition,
236191
creator,
237192
createdAt,
238193
}

0 commit comments

Comments
 (0)