This repository has been archived by the owner on Jan 17, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 3
/
Blockchain.ts
239 lines (219 loc) · 8.21 KB
/
Blockchain.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
import { hexToString } from '@polkadot/util'
import { ApiPromise } from '@polkadot/api'
import { KeyringPair } from '@polkadot/keyring/types'
import { Codec } from '@polkadot/types/types'
import { SubmittableExtrinsic } from '@polkadot/api/types'
import BlockchainError from './BlockchainError'
import { IBlockchainApi, IPortablegabiApi, PgabiModName } from '../types/Chain'
import Accumulator from '../attestation/Accumulator'
import { wasmStringify } from '../wasm/wasm_exec_wrapper'
/**
* The Blockchain class provides an interface for querying and creating transactions on chain.
*
* Example:
*
* ```js
* import portablegabi from '@kiltprotocol/portablegabi'
* // depending on the blockchain, the module where the accumulator is store might be called differently.
* // The name can be configured using the 'pgabiModName' option.
* const bc = await portablegabi.connect({ pgabiModName: 'portablegabiPallet' })
* const acc = await bc.getAccumulatorCount(addr)
* ```
*/
export default class Blockchain implements IBlockchainApi {
public api: ApiPromise & IPortablegabiApi<PgabiModName>
private chainmod: PgabiModName
/**
* Create a new Blockchain API instance.
*
* @param chainmod The name of the chain module that provides the portablegabi API.
* @param api The api connection to the chain.
*/
public constructor(
chainmod: PgabiModName,
api: ApiPromise & IPortablegabiApi<typeof chainmod>
) {
this.api = api as ApiPromise & IPortablegabiApi<typeof chainmod>
if (!(chainmod in api.query)) {
throw BlockchainError.missingModule(chainmod)
}
this.chainmod = chainmod as typeof chainmod
}
/**
* Get the number of stored [[Accumulator]]s for a specific [[Attester]].
*
* @param address The address of the [[Attester]].
* @returns The number of the [[Attester]]'s [[Accumulator]]s.
*/
public async getAccumulatorCount(address: string): Promise<number> {
const count = await this.api.query[this.chainmod].accumulatorCount(address)
return parseInt(count.toString(), 10)
}
/**
* Check for existing [[Accumulator]] count and return max index for query.
*
* @param address The chain address of the [[Attester]].
* @throws [[BlockchainError.maxIndexZero]] If the address does not have an [[Accumulator]] stored yet.
* @returns The accumulator count minus one.
*/
private async getMaxIndex(address: string): Promise<number> {
const maxIndex = (await this.getAccumulatorCount(address)) - 1
if (maxIndex < 0) {
throw BlockchainError.maxIndexZero(address)
}
return maxIndex
}
/**
* Check for existing [[Accumulator]] count and whether given index exceeds maxIndex.
*
* @param address The chain address of the [[Attester]].
* @param index The index of the [[Accumulator]].
* @throws [[BlockchainError.indexOutOfRange]] If the requested index is less than zero or greater than the maximum index.
*/
private async checkIndex(address: string, index: number): Promise<void> {
const maxIndex = await this.getMaxIndex(address)
if (index > maxIndex || index < 0) {
throw BlockchainError.indexOutOfRange(address, index, maxIndex)
}
}
/**
* Check whether codec is empty and convert codec->string->hex->accumulator.
*
* @param address The chain address of the [[Attester]].
* @param codec The raw [[Accumulator]].
* @param index The index of the [[Accumulator]].
* @throws [[BlockchainError.missingAccIndex]] If there is no [[Accumulator]] at the specified index.
* @returns An [[Accumulator]].
*/
private static async codecToAccumulator(
address: string,
codec: Codec,
index: number
): Promise<Accumulator> {
if (codec.isEmpty || (!codec.isEmpty && codec.toString().length < 2)) {
throw BlockchainError.missingAccAtIndex(address, index)
}
return new Accumulator(hexToString(codec.toString()))
}
/**
* Fetches a single [[Accumulator]] from the chain.
*
* @param address The on chain address of the [[Attester]].
* @param index The index of the [[Accumulator]] to fetch.
* @returns The [[Accumulator]] at the specified index.
*/
public async getAccumulator(
address: string,
index: number
): Promise<Accumulator> {
// check whether endIndex > accumulatorCount
await this.checkIndex(address, index)
// query accumulator at index
const codec: Codec = await this.api.query[this.chainmod].accumulatorList([
address,
index,
])
// convert codec to accumulator
return Blockchain.codecToAccumulator(address, codec, index)
}
/**
* Fetches multiple [[Accumulator]]s at once.
*
* @param address The chain address of the [[Attester]].
* @param startIndex The index of the first [[Accumulator]] to fetch.
* @param _endIndex The index of the last [[Accumulator]] to fetch.
* @returns An array of [[Accumulator]]s from startIndex to endIndex or the latest one.
*/
public async getAccumulatorArray(
address: string,
startIndex: number,
_endIndex?: number
): Promise<Accumulator[]> {
if (_endIndex) {
// check whether endIndex > accumulatorCount
await this.checkIndex(address, _endIndex)
}
const endIndex = _endIndex || (await this.getMaxIndex(address))
// create [[address, startIndex], ..., [address, endIndex]] for multi query
const multiQuery = new Array(endIndex - startIndex + 1)
.fill(startIndex)
.map((x, i) => [address, x + i])
// do multi query
const codecArr: Codec[] = await this.api.query[
this.chainmod
].accumulatorList.multi(multiQuery)
// convert codecs to accumulators
return Promise.all(
codecArr.map((codec, i) =>
Blockchain.codecToAccumulator(address, codec, i + startIndex)
)
)
}
/**
* Fetches the last published [[Accumulator]] for the specified [[Attester]].
*
* @param address The chain address of the [[Attester]].
* @returns The last published [[Accumulator]].
*/
public async getLatestAccumulator(address: string): Promise<Accumulator> {
const maxIndex = await this.getMaxIndex(address)
return this.getAccumulator(address, maxIndex)
}
/**
* Pushes a new [[Accumulator]] on chain.
*
* @param accumulator The new [[Accumulator]].
* @returns Returns an object that can be used to submit a transaction.
*/
public buildUpdateAccumulatorTX(
accumulator: Accumulator
): SubmittableExtrinsic<'promise'> {
return this.api.tx[this.chainmod].updateAccumulator(
wasmStringify(accumulator)
)
}
/**
* Signs and sends a transaction to a blockchain node.
*
* @param tx The transaction that should get submitted.
* @param keypair The keypair used for signing the transaction.
* @returns The returned promise will resolve if the transaction was included in a finalized block.
* If the transaction fails, the promise will be rejected.
*/
// If we have the object -> we have a connection -> we can submit
// makes sense to require an object for this method even tho we don't use this
// eslint-disable-next-line class-methods-use-this
public async signAndSend(
tx: SubmittableExtrinsic<'promise'>,
keypair: KeyringPair
): Promise<void> {
return new Promise((resolve, reject) => {
// store the handle to remove subscription.
let unsubscribe: (() => void) | null = null
// sign and send transaction
tx.signAndSend(keypair, (r) => {
// if block containing the transaction was finalized, check if transaction was successful
if (r.status.isFinalized) {
if (unsubscribe !== null) unsubscribe()
const sysEvents = r.events.filter(
({ event: { section } }) => section === 'system'
)
// filter for error events
const errEvents = sysEvents.filter(
({ event: { method } }) => method === 'ExtrinsicFailed'
)
// filter for success events
const okEvents = sysEvents.filter(
({ event: { method } }) => method === 'ExtrinsicSuccess'
)
// if there is no error and no success event, we fail
if (errEvents.length > 0) reject(errEvents)
else if (okEvents.length > 0) resolve()
else reject()
}
}).then((u) => {
unsubscribe = u
})
})
}
}