-
Notifications
You must be signed in to change notification settings - Fork 24
/
CType.chain.ts
251 lines (228 loc) · 7.66 KB
/
CType.chain.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/**
* Copyright (c) 2018-2023, BOTLabs GmbH.
*
* This source code is licensed under the BSD 4-Clause "Original" license
* found in the LICENSE file in the root directory of this source tree.
*/
import type { ApiPromise } from '@polkadot/api'
import type { Bytes, GenericCall, Option } from '@polkadot/types'
import type { AccountId, Call } from '@polkadot/types/interfaces'
import type { BN } from '@polkadot/util'
import type { CtypeCtypeEntry } from '@kiltprotocol/augment-api'
import type { CTypeHash, DidUri, ICType } from '@kiltprotocol/types'
import { ConfigService } from '@kiltprotocol/config'
import * as Did from '@kiltprotocol/did'
import { SDKErrors } from '@kiltprotocol/utils'
import {
getHashForSchema,
hashToId,
idToHash,
serializeForHash,
verifyDataStructure,
} from './CType.js'
import {
type DidAuthorizationCall,
flattenCalls,
isBatch,
retrieveExtrinsicFromBlock,
} from '../utils.js'
/**
* Encodes the provided CType for use in `api.tx.ctype.add()`.
*
* @param cType The CType to write on the blockchain.
* @returns Encoded CType.
*/
export function toChain(cType: ICType): string {
return serializeForHash(cType)
}
/**
* Encodes the provided CType['$id'] for use in `api.query.ctype.ctypes()`.
*
* @param cTypeId The CType id to translate for the blockchain.
* @returns Encoded CType id.
*/
export function idToChain(cTypeId: ICType['$id']): CTypeHash {
return idToHash(cTypeId)
}
// Transform a blockchain-formatted CType input (represented as Bytes) into the original [[ICType]].
// It throws if what was written on the chain was garbage.
function cTypeInputFromChain(input: Bytes): ICType {
try {
// Throws on invalid JSON input. CType is expected to be a valid JSON document.
const reconstructedObject = JSON.parse(input.toUtf8())
// Re-compute the ID to validate the resulting ICType.
const reconstructedCTypeId = hashToId(getHashForSchema(reconstructedObject))
const reconstructedCType: ICType = {
...reconstructedObject,
$id: reconstructedCTypeId,
}
// If throws if the input was a valid JSON but not a valid CType.
verifyDataStructure(reconstructedCType)
return reconstructedCType
} catch (cause) {
throw new SDKErrors.CTypeError(
`The provided payload cannot be parsed as a CType: ${input.toHuman()}`,
{ cause }
)
}
}
/**
* The details of a CType that are stored on chain.
*/
export interface CTypeChainDetails {
/**
* The DID of the CType's creator.
*/
creator: DidUri
/**
* The block number in which the CType was created.
*/
createdAt: BN
}
export type ICTypeDetails = { cType: ICType } & CTypeChainDetails
/**
* Decodes the CType details returned by `api.query.ctype.ctypes()`.
*
* @param encoded The data from the blockchain.
* @returns An object indicating the CType creator.
*/
export function fromChain(
encoded: Option<AccountId>
): Pick<CTypeChainDetails, 'creator'>
/**
* Decodes the CType details returned by `api.query.ctype.ctypes()`.
*
* @param encoded The data from the blockchain.
* @returns An object indicating the CType creator and createdAt block.
*/
export function fromChain(encoded: Option<CtypeCtypeEntry>): CTypeChainDetails
// eslint-disable-next-line jsdoc/require-jsdoc
export function fromChain(
encoded: Option<CtypeCtypeEntry> | Option<AccountId>
): CTypeChainDetails | Pick<CTypeChainDetails, 'creator'> {
const unwrapped = encoded.unwrap()
if ('creator' in unwrapped && 'createdAt' in unwrapped) {
const { creator, createdAt } = unwrapped
return {
creator: Did.fromChain(creator),
createdAt: createdAt.toBn(),
}
}
return {
creator: Did.fromChain(unwrapped),
}
}
// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.ctype.add`.
function extractCTypeCreationCallsFromDidCall(
api: ApiPromise,
call: Call
): Array<GenericCall<typeof api.tx.ctype.add.args>> {
const extrinsicCalls = flattenCalls(api, call)
return extrinsicCalls.filter(
(c): c is GenericCall<typeof api.tx.ctype.add.args> =>
api.tx.ctype.add.is(c)
)
}
// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.did.submitDidCall` or `api.tx.did.dispatchAs`.
function extractDidCallsFromBatchCall(
api: ApiPromise,
call: Call
): DidAuthorizationCall[] {
const extrinsicCalls = flattenCalls(api, call)
return extrinsicCalls.filter(
(c): c is DidAuthorizationCall =>
api.tx.did.submitDidCall.is(c) || api.tx.did.dispatchAs.is(c)
)
}
/**
* Resolves a CType identifier to the CType definition by fetching data from the block containing the transaction that registered the CType on chain.
*
* @param cTypeId CType ID to use for the query. It is required to complement the information stored on the blockchain in a [[CtypeCtypeEntry]].
*
* @returns The [[ICTypeDetails]] as the result of combining the on-chain information and the information present in the tx history.
*/
export async function fetchFromChain(
cTypeId: ICType['$id']
): Promise<ICTypeDetails> {
const api = ConfigService.get('api')
const cTypeHash = idToHash(cTypeId)
const cTypeEntry = await api.query.ctype.ctypes(cTypeHash)
const { createdAt } = fromChain(cTypeEntry)
if (typeof createdAt === 'undefined')
throw new SDKErrors.CTypeError(
'Cannot fetch CType definitions on a chain that does not store the createdAt block'
)
const extrinsic = await retrieveExtrinsicFromBlock(
api,
createdAt,
({ events }) =>
events.some(
(event) =>
api.events.ctype.CTypeCreated.is(event) &&
event.data[1].toString() === cTypeHash
)
)
if (extrinsic === null) {
throw new SDKErrors.CTypeError(
`There is not CType with the provided ID "${cTypeId}" on chain.`
)
}
if (
!isBatch(api, extrinsic) &&
!api.tx.did.submitDidCall.is(extrinsic) &&
!api.tx.did.dispatchAs.is(extrinsic)
) {
throw new SDKErrors.PublicCredentialError(
'Extrinsic should be either a `did.submitDidCall` or `did.dispatchAs` extrinsic or a batch containing at least one of those'
)
}
// If we're dealing with a batch, flatten any nested `submit_did_call` calls,
// otherwise the extrinsic is itself a submit_did_call, so just take it.
const didCalls = isBatch(api, extrinsic)
? extrinsic.args[0].flatMap((batchCall) =>
extractDidCallsFromBatchCall(api, batchCall)
)
: [extrinsic]
// From the list of DID calls, only consider ctype::add calls, bundling each of them with their DID submitter.
// It returns a list of [reconstructedCType, attesterDid].
const ctypeCallContent = didCalls.flatMap((didCall) => {
const ctypeCreationCalls = extractCTypeCreationCallsFromDidCall(
api,
api.tx.did.submitDidCall.is(didCall)
? didCall.args[0].call
: didCall.args[1]
)
// Re-create the issued public credential for each call identified.
return ctypeCreationCalls.map(
(ctypeCreationCall) =>
[
cTypeInputFromChain(ctypeCreationCall.args[0]),
Did.fromChain(
api.tx.did.submitDidCall.is(didCall)
? didCall.args[0].did
: didCall.args[0]
),
] as const
)
})
// If more than a call is present, it always considers the last one as the valid one.
const lastRightCTypeCreationCall = ctypeCallContent
.reverse()
.find((cTypeInput) => {
return cTypeInput[0].$id === cTypeId
})
if (!lastRightCTypeCreationCall) {
throw new SDKErrors.CTypeError(
'Block should always contain the full CType, eventually.'
)
}
const [ctypeInput, creator] = lastRightCTypeCreationCall
return {
cType: {
...ctypeInput,
$id: cTypeId,
},
creator,
createdAt,
}
}