From c9f9ada75d7e89bad7ee834cb897f498010b323d Mon Sep 17 00:00:00 2001 From: Victor Oliva Date: Mon, 9 Sep 2024 01:00:25 +0200 Subject: [PATCH] JSON RPC interface spec: chainHead_v1 (#813) * add chainHead follow + unfollow * add chainHead_v1_header * implement chainHead_v1_call * start storage * start storage * add body + function doc headers * export response types * fix call errors not propagating through RPC * add tests for chainHead_v1 * change to block with metadata v15, separate papi into another function * migrate to observable client * fix build-block tests --- packages/core/src/rpc/index.ts | 2 + .../core/src/rpc/rpc-spec/chainHead_v1.ts | 312 ++++++++++++++++++ packages/core/src/rpc/rpc-spec/index.ts | 9 + packages/e2e/package.json | 4 + .../__snapshots__/chainHead_v1.test.ts.snap | 134 ++++++++ packages/e2e/src/build-block.test.ts | 6 +- packages/e2e/src/chainHead_v1.test.ts | 119 +++++++ packages/e2e/src/helper.ts | 178 ++++++++-- yarn.lock | 93 +++++- 9 files changed, 820 insertions(+), 37 deletions(-) create mode 100644 packages/core/src/rpc/rpc-spec/chainHead_v1.ts create mode 100644 packages/core/src/rpc/rpc-spec/index.ts create mode 100644 packages/e2e/src/__snapshots__/chainHead_v1.test.ts.snap create mode 100644 packages/e2e/src/chainHead_v1.test.ts diff --git a/packages/core/src/rpc/index.ts b/packages/core/src/rpc/index.ts index 8ef78d1e..254fbe97 100644 --- a/packages/core/src/rpc/index.ts +++ b/packages/core/src/rpc/index.ts @@ -1,9 +1,11 @@ import { Handlers } from './shared.js' import dev from './dev/index.js' +import rpcSpec from './rpc-spec/index.js' import substrate from './substrate/index.js' export const allHandlers: Handlers = { ...substrate, + ...rpcSpec, ...dev, rpc_methods: async () => Promise.resolve({ diff --git a/packages/core/src/rpc/rpc-spec/chainHead_v1.ts b/packages/core/src/rpc/rpc-spec/chainHead_v1.ts new file mode 100644 index 00000000..946b5f7b --- /dev/null +++ b/packages/core/src/rpc/rpc-spec/chainHead_v1.ts @@ -0,0 +1,312 @@ +import { Block } from '../../blockchain/block.js' +import { Handler, ResponseError, SubscriptionManager } from '../shared.js' +import { HexString } from '@polkadot/util/types' +import { defaultLogger } from '../../logger.js' + +const logger = defaultLogger.child({ name: 'rpc-chainHead_v1' }) + +const callbacks = new Map void>() + +async function afterResponse(fn: () => void) { + await new Promise((resolve) => setTimeout(resolve, 0)) + fn() +} + +/** + * Start a chainHead follow subscription + * + * @param context + * @param params - [`withRuntime`] + * @param subscriptionManager + * + * @return subscription id + */ +export const chainHead_v1_follow: Handler<[boolean], string> = async ( + context, + [withRuntime], + { subscribe }: SubscriptionManager, +) => { + const update = async (block: Block) => { + logger.trace({ hash: block.hash }, 'chainHead_v1_follow') + + const getNewRuntime = async () => { + const [runtime, previousRuntime] = await Promise.all([ + block.runtimeVersion, + block.parentBlock.then((b) => b?.runtimeVersion), + ]) + const hasNewRuntime = + runtime.implVersion !== previousRuntime?.implVersion || runtime.specVersion !== previousRuntime.specVersion + return hasNewRuntime ? runtime : null + } + const newRuntime = withRuntime ? await getNewRuntime() : null + + callback({ + event: 'newBlock', + blockHash: block.hash, + parentBlockHash: (await block.parentBlock)?.hash, + newRuntime, + }) + callback({ + event: 'bestBlockChanged', + bestBlockHash: block.hash, + }) + callback({ + event: 'finalized', + finalizedBlockHashes: [block.hash], + prunedBlockHashes: [], + }) + } + + const id = context.chain.headState.subscribeHead(update) + + const cleanup = () => { + context.chain.headState.unsubscribeHead(id) + callbacks.delete(id) + } + + const callback = subscribe('chainHead_v1_followEvent', id, cleanup) + callbacks.set(id, callback) + + afterResponse(async () => { + callback({ + event: 'initialized', + finalizedBlockHashes: [context.chain.head.hash], + finalizedBlockRuntime: withRuntime ? await context.chain.head.runtimeVersion : null, + }) + }) + + return id +} + +/** + * Stop a chainHead follow subscription + * + * @param context + * @param params - [`followSubscription`] + * @param subscriptionManager + */ +export const chainHead_v1_unfollow: Handler<[string], null> = async (_, [followSubscription], { unsubscribe }) => { + unsubscribe(followSubscription) + + return null +} + +/** + * Retrieve the header for a specific block + * + * @param context + * @param params - [`followSubscription`, `hash`] + * + * @return SCALE-encoded header, or null if the block is not found. + */ +export const chainHead_v1_header: Handler<[string, HexString], HexString | null> = async ( + context, + [followSubscription, hash], +) => { + if (!callbacks.has(followSubscription)) return null + const block = await context.chain.getBlock(hash) + + return block ? (await block.header).toHex() : null +} + +type OperationStarted = { + result: 'started' + operationId: string +} +const operationStarted = (operationId: string): OperationStarted => ({ result: 'started', operationId }) +const randomId = () => Math.random().toString(36).substring(2) + +/** + * Perform a runtime call for a block + * + * @param context + * @param params - [`followSubscription`, `hash`, `function`, `callParameters`] + * + * @return OperationStarted event with operationId to receive the result on the follow subscription + */ +export const chainHead_v1_call: Handler<[string, HexString, string, HexString], OperationStarted> = async ( + context, + [followSubscription, hash, method, callParameters], +) => { + const operationId = randomId() + + afterResponse(async () => { + const block = await context.chain.getBlock(hash) + + if (!block) { + callbacks.get(followSubscription)?.({ + event: 'operationError', + operationId, + error: `Block ${hash} not found`, + }) + } else { + try { + const resp = await block.call(method, [callParameters]) + callbacks.get(followSubscription)?.({ + event: 'operationCallDone', + operationId, + output: resp.result, + }) + } catch (ex: any) { + callbacks.get(followSubscription)?.({ + event: 'operationError', + operationId, + error: ex.message, + }) + } + } + }) + + return operationStarted(operationId) +} + +export type StorageStarted = OperationStarted & { discardedItems: number } +export interface StorageItemRequest { + key: HexString + type: 'value' | 'hash' | 'closestDescendantMerkleValue' | 'descendantsValues' | 'descendantsHashes' +} + +/** + * Query the storage for a given block + * + * @param context + * @param params - [`followSubscription`, `hash`, `items`, `childTrie`] + * + * @return OperationStarted event with operationId to receive the result on the follow subscription + */ +export const chainHead_v1_storage: Handler< + [string, HexString, StorageItemRequest[], HexString | null], + StorageStarted +> = async (context, [followSubscription, hash, items, _childTrie]) => { + const operationId = randomId() + + afterResponse(async () => { + const block = await context.chain.getBlock(hash) + if (!block) { + callbacks.get(followSubscription)?.({ + event: 'operationError', + operationId, + error: 'Block not found', + }) + return + } + + const handleStorageItemRequest = async (sir: StorageItemRequest) => { + switch (sir.type) { + case 'value': { + const value = await block.get(sir.key) + if (value) { + callbacks.get(followSubscription)?.({ + event: 'operationStorageItems', + operationId, + items: [{ key: sir.key, value }], + }) + } + break + } + case 'descendantsValues': { + // TODO expose pagination + const pageSize = 100 + let startKey: string | null = '0x' + while (startKey) { + const keys = await block.getKeysPaged({ + prefix: sir.key, + pageSize, + startKey, + }) + startKey = keys[pageSize - 1] ?? null + + const items = await Promise.all( + keys.map((key) => + block.get(key).then((value) => ({ + key, + value, + })), + ), + ) + callbacks.get(followSubscription)?.({ + event: 'operationStorageItems', + operationId, + items, + }) + break + } + break + } + default: + // TODO + console.warn(`Storage type not implemented ${sir.type}`) + } + } + + await Promise.all(items.map(handleStorageItemRequest)) + + callbacks.get(followSubscription)?.({ + event: 'operationStorageDone', + operationId, + }) + }) + + return { + ...operationStarted(operationId), + discardedItems: 0, + } +} + +export type LimitReached = { result: 'limitReached' } +const limitReached: LimitReached = { result: 'limitReached' } + +/** + * Retrieve the body of a specific block + * + * @param context + * @param params - [`followSubscription`, `hash`] + * + * @return OperationStarted event with operationId to receive the result on the follow subscription + */ +export const chainHead_v1_body: Handler<[string, HexString], OperationStarted | LimitReached> = async ( + context, + [followSubscription, hash], +) => { + if (!callbacks.has(followSubscription)) return limitReached + const block = await context.chain.getBlock(hash) + if (!block) { + throw new ResponseError(-32801, 'Block not found') + } + + const operationId = randomId() + afterResponse(async () => { + const body = await block.extrinsics + + callbacks.get(followSubscription)?.({ + event: 'operationBodyDone', + operationId, + value: body, + }) + }) + + return operationStarted(operationId) +} + +// Currently no-ops, will come into play when pagination is implemented +export const chainHead_v1_continue: Handler<[string, HexString], null> = async ( + _context, + [_followSubscription, _operationId], +) => { + return null +} + +export const chainHead_v1_stopOperation: Handler<[string, HexString], null> = async ( + _context, + [_followSubscription, _operationId], +) => { + return null +} + +// no-op, since there's no concept of unpinning in chopsticks +export const chainHead_v1_unpin: Handler<[string, HexString | HexString[]], null> = async ( + _context, + [_followSubscription, _hashOrHashes], +) => { + return null +} diff --git a/packages/core/src/rpc/rpc-spec/index.ts b/packages/core/src/rpc/rpc-spec/index.ts new file mode 100644 index 00000000..69c04eb5 --- /dev/null +++ b/packages/core/src/rpc/rpc-spec/index.ts @@ -0,0 +1,9 @@ +import * as ChainHeadV1RPC from './chainHead_v1.js' + +export { ChainHeadV1RPC } + +const handlers = { + ...ChainHeadV1RPC, +} + +export default handlers diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 75196558..8ac76c6e 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -13,7 +13,11 @@ "devDependencies": { "@acala-network/chopsticks": "workspace:*", "@acala-network/chopsticks-testing": "workspace:*", + "@polkadot-api/observable-client": "^0.5.3", + "@polkadot-api/substrate-client": "^0.2.1", + "@polkadot-api/ws-provider": "^0.2.0", "@polkadot/api": "^12.3.1", + "rxjs": "^7.8.1", "typescript": "^5.5.3", "vitest": "^1.4.0" } diff --git a/packages/e2e/src/__snapshots__/chainHead_v1.test.ts.snap b/packages/e2e/src/__snapshots__/chainHead_v1.test.ts.snap new file mode 100644 index 00000000..19e0268a --- /dev/null +++ b/packages/e2e/src/__snapshots__/chainHead_v1.test.ts.snap @@ -0,0 +1,134 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`chainHead_v1 rpc > reports the chain state 1`] = ` +{ + "finalizedBlockHashes": [ + "0x6c74912ce35793b05980f924c3a4cdf1f96c66b2bedd0c7b7378571e60918145", + ], + "finalizedBlockRuntime": { + "apis": [ + [ + "0xdf6acb689907609b", + 5, + ], + [ + "0x37e397fc7c91f5e4", + 2, + ], + [ + "0x40fe3ad401f8959a", + 6, + ], + [ + "0xd2bc9897eed08f15", + 3, + ], + [ + "0xf78b278be53f454c", + 2, + ], + [ + "0xdd718d5cc53262d4", + 1, + ], + [ + "0xab3c0572291feb8b", + 1, + ], + [ + "0xbc9d89904f5b923f", + 1, + ], + [ + "0x37c8bb1350a9a2a8", + 4, + ], + [ + "0x6ef953004ba30e59", + 1, + ], + [ + "0x955e168e0cfb3409", + 1, + ], + [ + "0x9af86751b70c112d", + 2, + ], + [ + "0xe3df3f2aa8a5cc57", + 2, + ], + [ + "0xea93e3f16f3d6962", + 2, + ], + ], + "authoringVersion": 1, + "implName": "acala", + "implVersion": 0, + "specName": "acala", + "specVersion": 2250, + "stateVersion": 0, + "transactionVersion": 3, + }, + "type": "initialized", +} +`; + +exports[`chainHead_v1 rpc > resolves storage queries 1`] = ` +{ + "consumers": 0, + "data": { + "flags": 0n, + "free": 213800847850000n, + "frozen": 0n, + "reserved": 0n, + }, + "nonce": 0, + "providers": 1, + "sufficients": 0, +} +`; + +exports[`chainHead_v1 rpc > resolves the header for a specific block 1`] = ` +{ + "digests": [ + { + "type": "preRuntime", + "value": { + "engine": "aura", + "payload": "0x723f900800000000", + }, + }, + { + "type": "consensus", + "value": { + "engine": "RPSR", + "payload": "0x2860b2165e66da4ac9bc708888850161ae98a09cc17dd4bf11748006d7591ab1464b4805", + }, + }, + { + "type": "seal", + "value": { + "engine": "aura", + "payload": "0xc8cac1f7cd74ee2d23687b086a1b460c95105b15d617e4b397229a18d56f7f36f2238a5454d541431e73e52d83242a062ab709a7e32213ba3fbc098a179f9a89", + }, + }, + ], + "extrinsicRoot": "0x4e5803caed34cae3c042eb27184d5f6f821a298bb39a8e328609ef172160ac94", + "number": 6800000, + "parentHash": "0xa4052fe0e02be39765c5c2c565f4f3f14bbead21ef2d58174c77c544b96b7c9d", + "stateRoot": "0x9d51466c1d3d5ba90cca4ca887a7532a36dca3cd06f85334346f1345b412c77e", +} +`; + +exports[`chainHead_v1 rpc > retrieves the body for a specific block 1`] = ` +[ + "0x280401000bc0ff9d699101", + "0xa2130100041e00910388af62e668997722826b847423c55fa662bc3e8e8e879bb70ec9601e7ddc368afe099f01943ada41dfe46d6fc6fae3af26ab713ce32764bf8b74ec7a79a067e5ff8d041d5731a40e6e865073580f5573124e33dd1fd527d02bf651fdd7103895acb374b60c0661757261206f3f9008000000000452505352906930032dd33b0d421cf40d62865c3ff214198418450934c0e14e882cdb1b60d02e4b4805056175726101010083276d51990521178f1beccbaba213ff8d9821ec4a3184ebeff14d8ad315452cc820fafdd446946ae71ae0ec4c6dd7257a51b660752a5b4619e77ad91e2988d11252012860b2165e66da4ac9bc708888850161ae98a09cc17dd4bf11748006d7591ab100005000e901b9030000300000500000aaaa020000001000fbff0000100000000a000000403800005802000003000000020000000000500000c800008000000000e8764817000000000000000000000000e87648170000000000000000000000e80300000090010080000000009001000c01002000000600c4090000000000000601983a000000000000403800000000000000000000f401000080b2e60e80c3c90180b2e60e000000000000000000000000050000000a0000000a0000000200000001050000000006000000580200000300000059000000000000001e000000060000000200000014000000020000000803060000008401aec4b01db7956fe9afc5ea1422f3863b5c4ac5e7d1ca01de40feec8ee8ddd1d1c430e8030000d4070000d6070000d8070000db070000dc070000e9070000f0070000f2070000f3070000f507000038080000b036f5a4efb16ffa83d00700001bc7f476dfe707235fa130352f6d6d7b8f6af260d4604a80c0409dce936de7deb036f5a4efb16ffa83d0070000cb39271af6d425bf325d065c38ba81fdaf1a5dd514212180615278654d129e13bc3c36708366b722d0070000e803000073bb85503c9ce8a5c5de9fbc9f5d1d3995dc8a01d7f92b57b86b949278610d5ebc3c61975d97255ddb070000d0070000cb69ff2cc961a8ca5ff59ce0011e5fac4fed2c9b2e5eaee8842b10762a3bffacbc3c8c2de8299067f3070000d0070000e45d32ea4e0ec13ed2e8e6283aa9575c4c01c522de825ed3d10fd5fbc991ba88bc3ce1ee506d55f8d0070000f30700005655af9dc44f724eeba278f3471bd15c8d0c700d9d7efeb8cd08a916875ff31ac03d01055ad63302efd0070000e90700007fb68490ee9a0d34e1fc4c0afccbe6de199b43de888ecee90f3f73b940d9666bc03d01c872d0ebaf85d0070000f5070000d6d78c4cbae9e564b49ed18676e1949d6c7f2108e6f5f7fad0356c23e15f50bcc03d0452a22bee61fad0070000f2070000080a7566d61eb98f5f17792fc9d2ab8deaee9ff93d783f0395a00ab51036a053c03d04d2a15ab51127e8030000d007000066ce5bf31dc29eef2dfba78871a6e34201610fbe426456c9611b72febd32dba4c03d051e98d504f202e9070000d00700004fc62192ff6fb43e82895a41af1359c1b524ab006a2f3fe2a544024a57390d7ac03d057a605f506cfcd4070000d0070000b8a9cc865d1818b0b945fee261c457421a8efcf83fb73bb23d968cef0acdad5bc03d06ad4314650419d0070000f00700004bc1aa4b4d45b3185a97e6344d9a6bd3703d8136dc29ae73597e41f858153d49c03d07edc4cbc65e03d0070000d40700004171e8dcaee73265330d2a7865174e4cfb1f8d0fe26421343232877d42c094e0c03d099274c2ff3639d0070000d607000053b8449b8f173e2ba3c5e4b5166032aef686e4e9c74fc408e73d36e59557fae2c03d0a87461a6f93a938080000d0070000994bfa94334cfeb0c3e8ba920625786d5a6461d1938e8f5dda816d545d7d1a46c03d0b652b2ae6ed1ddc070000d007000054e527a8162ceb801f466586bf6867caf7918f71268f65ea7e17179faf53da03c03d0bc334ef110d8af5070000d0070000af58a73479f505c28d5163d3b243c5197c8b6e1867c42f99786ada933b2f8e56c03d0c472775baca93f0070000d0070000ea2aa6e040a9c47e0c974dec9c998455067282856647f5728057566a324b78efc03d0fcde7347851edd007000038080000fb6290dd4be4ee168f0b2300500feb48b1e2f8710f4e60ee0c4f5ab340fb5caec03e046fae65527199f2070000d007000027e94a9f3cb02f618d71259a55fbb5b3fb5ad53cda086d37e1c6450d97ec9a9cc03e1a4e21b9b6ce11d8070000d00700005fe20ae7a7fda47e20b9a1438f410072c8cf061e3ad1a8101fd62e0f0cc6ba09c03e36c65ca123d5fbd0070000dc070000a8a24511a6f7adb23c20b64c0a8dfe509f88753069bccc4a52e82d74faff865ac03e55c8e02d73966fd6070000d007000095cf63c4a522c3381b802d135c082073991f1152b4224f681b5abe506b8d3a48c03e77dfdb8adb10f78f10a5df8742c5456e76c62ea100f44d2eccd954bf915b53e5801c6cd0115e3792b8bc80e43214fbc03ec412f496dcf830d0070000d8070000e673d8e6cf2fb0c0e72969c755c838eb33650c98c9be1c0ac1a88c2a72c4aa2ac03ec648b30353eed1d0070000db070000ba0b46a547fc8d75d891d2c86cc37c355a86ff648ff1bd964d78f4399e344c62c83f0004b49d95320d9021994c850f25b8e38560afd46d9983cc5de53321ba187493f417d4c9e4f4b521b2c2bb31d44a8443a3b456f5a4efb16ffa83d00700008027e34bd9f7996b21b6621117a60f23959132a8d0454a4f70bba1b6b6451290adc45e414cb008e0e61e46722aa60abdd67280501a8f02d815fe2d845f71828a8a9d16c3be86419b6cbea5b19f4e07c362328bc45ee678799d3eff024253b90e84927cc68049027bdf2d245b1c2b86eb63317f361d89f8fe33d5c3afb616d4fc9b84b28d09150180000a80891793cd20d66b9394617c7f9391ea649c9d2f1d1555cd8363cc5129b90615e5805234437b1106ee51b6646de83d886d5fcd7ebba94498177830eb8150559057f61501800014809f666f6d01d5e8126ac0c9156bd3bd78a72a681031c85a282e8ed25a9b646117805ba2f36dca0a7fefb81e2c632b95b87733cb49f28ac84f78c37e7010ba76e5b6d48000c0787694c040f5e73d9b7addd6cb603d15d3b0021d9da3ceafbd080d00000401485e4993f016e2d2f8e5f43be7bb2594860400150180010180962557aa2a950dd69ba2a7bc387090d2e254739e0e7920219e4e41dc57d8633980cc82766c631fc89b7a7f7c148fa41e391c8ba734c2e13213c61b07904ae036c615018001028007d384c8f98ce935ddb4eb515cebfa1a955956442f073deac670726caf1f848c802008daa08f23a5715e1096a34b5972effaf8f03864b39ce433b1be362527bfa615018001108084ef397bb6136701684b978cfd2f245e02d8b93a18ccf130c1b28220da89e000805f2d610e5ebee6f8df0b2c10355be9fa82a0d565b83156a082948907e1b25314150180011080e2e1073c8ec364af0f4ba116e6e35297683ce811cd2ebc97c285d2ddd84c8016800ebe650369ce0e3ce85b22816181f598cc6ecf7a3b22314357ab4181b9ccb8671d028002a480d6271a4c2a039dde0fa546dc399ed48e95a8fb69893042beeea184dd1d9f3b488084d7b1f67271dfb1c2c5b92012867b514f19537d8f96b8a743fcee0d4c55e97880eab552bf50fc9e3b0d712949fe21eeb07ffc43aa600af2d9cb868901365c303280765d5fe8fe2a2cbfe69d8b70daff4c1c761b04eb5e78afae256f9c118d40ea07150180040480071f0c0e2250bc87bebbcd3c88e9705afdf2368a6abfa5e12b7c4756be306003800fe6b03639ff2739e42b263a64b97458bbb7b04a7f13a7dd10b77ba5c3d3940e15018004048014e25275458e6d757846b2b1af3fe7577b04e472e3b60f74b777518431a8106780ccb42654456a3f8042afd95fbcbfd30e782d9b748f8ed5c9ae6093b69dfcfe511d0280046480fa2f1c9b5a63a4209bb67590e1b79afac17cfe954c7cf2f40ec6032970e1bfcf8017c559dff26192cf052837047fedf02780a62adf22a014d2b318d6a414c52161801aae7b2ee4927d2dcb44d74159e61146cf8a7b89a3bf2a2651f83897f9f4ec6380e90f0cc58f13b27178d75c8751d0314441e9c9ab602c3a9de5d4713510d2b3861501800802807e9e4b19a200293a03a05f42074a9fcbca0ce76d4b5f19faf9bd5df55260547f8057c1687622de7c7420b8b343f3369e1b275931ce7523403d728e2f1bcdba2b221d0280088c80f48a4cb3ef9a43743d7ea14fe588ac149b2ce52595dd48a7520931dde6b7f59780bb0907adef6404f6a77b52f1783a00d6b5fd2887d87871f9e99efd62b9e9a487801f43383f19eff2fdfb24499981d16df819e76470b3d9ad30a9d612762ac1c9b38012f9076f0838d615483a06778635d8a95f7856a958225d66b3acb23e8583fdfa1501800a00807f57b51f940a99eb47a01e8bee249226f1b7346093f2a6be2692cfc13f5ff6b2803657010e635306740f35a4b95ef61c0f5c7a02680d606591b6f1f9a247d339119901800c208000d078ead84fe50d7c57fae046bbe26fefcf600030423c02d9cd158b60383a6e803878a214d01c9ee05d9a79af93386b2891123e23f5f271401c52462a83d9205b80285e65520e8d0f5a33ac48c4800057e990da0022cd781a3d6ea6130e5776517c1501800c4054568cdace56b4195be90700002000000000000000005456f5a4efb16ffa83d0070000200000000000000000545628b8ad2696ed532c080000200000000000000000b501800c5068568cdace56b4195be9070000340ce8030000d0070000d60700008094ab9c828b8bfba383bef89f81ebb8466733eff45bec0ed12a6c73bd185c232f48563524f87eaf5eaf000800001404e8030000685628b8ad2696ed532c080000340ce8030000d4070000f20700001d02800c5080509c6b0aa1e01144f8b0668107839dfb1826dde88fa19d6405355e6513ac8cd980ddc70efef8abce69210bdd3d98e43984d67542ac5516d61cc8a9c869594ce54b80f0d767cecf4e90500226e8bb671760b7873affbb0d19b0593fc13b076a171ffe802d400df0bbd47758c8de53f7570d98e24046ea6b3b856b84e73cd49fb56d082e1d02800c5080624fb3c3e26ce9171280ed05bedcea733d0d6ac3f09eef4dac99935fdc4c182280f7b76816a23c7ea27c8abcded6f1b71a4f082dfecd2360787e4ca9a0a5b93e4b80aa355c7789c126573837fde95fd60e274cddf219667c77dafaf68056bda3be668014562f1689cdc760404567749e2a94e585b145892d8a65533174645a293b379615018010028084cf03b73e1be9a8096b20cb1394beb7f7cf51d6325ea381a83c976e9802afd7809acfe6c9d163d17a52b1b63ffcd9b626ff846e2929f186bf1d53218c79a79245a10280101780861199e1bd8c4cae68bb8f907c359618e482becf6738de4c54021b14160d9dd580737f2b31b70310577a65679be43ca9d9dff168373c9f8c0e5a6db2f21094fb0a808e62d7380f3723ed14dff63170462036cffe8de4d1fb8174e6831264c07376b58096e221af260de43f33e172223bc935e60f091c34cf092437d2f95e78ef781e4280342811557fdf8f6f7e7e240da1e44b22e3892d6e6081b048753544385bed78fa990180106080893822d662072f04aeada28e7cb6d6c41b54820fda518b2f1c8c9f0a263052d88054c34614ff842b06738fba1c46e59c383587e8fb153c67903c338212de3bf15c80bf6798fbbc5efad451b2b59453b1fdb43e4c53cef1e5f3d88fec70b1dbcff38899018010c080ce9c9ca970c470e65bb27954d5eece437e5c786c84b3c74859dd4f44f8a9edc48054b96e7976c638bfa0324cbf19af33d2c99c1e992e20e42c0be318a67716550d8042a9c06ab28d305c3058f276bbddbb01c289a035afa16e493f6f9b9d7811717c1501801200806cdb0c79d84c2420f6331254faba0f0f973eb930f2b5db28f0afbef4eeb0eb7b80416c0ced709d98901f72d8acca13ae029e28706dad4f128e59083fd19c4259d21501801800805c4eb02dfaa3c523a2eb72d84be020e9fa9fb73c0c482d7bd7860c53eee0e97680bdf88be86fa17471a4be319a9ccb2bd70817bbbbf4be0a782bee07ab413be7152d0480364e80b9503d2ee1e094b79b5e3f9e924a5f83f0074554fa6ae3867350695d6a994f828004ab27e7a80aeafc5fb7978e918921ce8baf561fe6afa3ef1427604fa84f0b6c803fde5ef75bf6a438cf1bd91afc03c397b785a1e04857b03a94a4c85f4b25a71e80bb7f39b5d7d57105dcfa458c41d8fd997ffb1f5683f11abcff1b2829747b163a806029020943985e1c25c70398f0971430a40d40bde2e3aa9c8b9e2b08a58f9e918073dede1f95a025a268b23ead5e58350d69fe851fa37d34e7bddc9a8cdb3c56c880521ea74583ae82361092a171938a6794a35d6ef928ac19ddbe80077aef7f81d5809f16e087553933c2299e6b259e88beb952d0abc46d1c3756d5719f631e5d080a3d06803f7e8019a05bb18bc9454488f1a18f6e8b84b8de3acb3eac2101900d166d33ff67d30080951da52898d069e876c6e930789f4b5806ee702b9a46564ab7bd4875893b702a804d4427633c854ef7c1a337b8f96cd23148f1dd6deb80b1675ae0dad6e164132680ffa08983d143637098114d7ae6abcefd6254d44b7d2fed7a1ffaf184305d49d8808a562733b218376b88b8749d2d28f3f7f74e1054b53bca119a7d4e53269c2760806ad572c7e072147d947a87e217ea3b0e644973b590891fe5168fd2f3e5462fd78095c8ee7a6f7a0eb49b04fa011ae43f6bdef40d01608e73bc4551c8cd731236e0807eca444114244b8f719d00a7141acf103b8feed07980fab3aa4545d9374b2c3f8031248f779ef5a85a3b93c843e9f0a07a653e4d2a531f4b3c512dfa08e40fe4f68042b8cc782ade570f3e6cc3909a03481ad91d48f01705346f1f20b59ead88c1eb80111ae3f2373f02508e55bdc89d9dccd3b112e6a7e567e367a997f949098a51b480ae5fc4841bb4c68eed66384c9379730aea5ab6d8ba07388779749774d86ea9f7a10280411980500e10a07888f5662e85f3726873d8bd63e3160ee54364d7e37dbc339c894b918083c1ca317354535edf466e0c85a69d2659304dc04b3ae29d6f76df0f8b1adc4c8090de2de1831b429a52a7d5e1786231bce739428b037c80bd066a570d7ed8c35780defdb9642b7f9f6dced5060bc978de7ae9850fa9f3e9ee6112a2c52a49242a3c80de524a9c1d8553deda717b591e11d592b55806b99e39c0b7c22602eb9ae42acd9501805139344607000020aaaa020000001000344607000020aaaa020000001000344607000020aaaa020000001000344607000020a8aa020050ff0f00344607000020aaaa020000001000344607000020aaaa020000001000344607000020aaaa020000001000990180610080b8197956122d2a6419bcd85c0c5e28136ad3000e610d0e1ee07bdf1122483c32804b9e7c8fce27de36c27572b2cb1357bf2d597376b46809e17640542d5bdd0a3e808c214cd9133e1ad6e511d8a77c4cb8429a4afaea7f5b26ce83832b97fa79e07b2d04806d388024ea729aca2db4ff58eea5487a72eb01713c3b8faa1e01cf4197022d344f0b078093b707ffa7fb4e7a6e175b99be23cdbbcf6473c268ff7f49720b3de9a7ed38bf80232c3196ecfcfc718c91aebe32d7f1411e56d1e25b70dd85f32f15300b75811080c03862c9e8301b4432321e0fb9a8e142a728e4f9f4ff137e5d48fe1729cacf1480f8345001ab5ec7fb84123937d191f88282eb1d4392853d57c4e3df47045edd5880d47d432aa005a0fd9cf88fc63f4268c4ed52c6f9a1bb7419675119f877ce989280d3675443de7490416707fbf73fafaf0832969981d0b4766a70d7e1732b59eee88026357edbd5dea4d3371110c96e6482d3b619835ee25ff19a693558f10305a158b1048078d3802ffac70e40364bd782830746c33af5d62963e9cfa55d783868551bbd1313d198803796737038e1b6d2447fc85f42c300ab2f413cae7d93ff8d130b1a438491b35280fa37df40952dd6c2dfbfb96122da0dabdd1c9185706f0613da29a079ee677662805debec2e1c9002d11fe7060752955a39eb770e41536d127867758f04c393a9ba808be0f66504109883f96cfab47a8f91e28e30d3cc002ac97499fa2beb826b2d0e80b3608dc53a625f68bf69b2edd5df196f83438665ead3dcaf25a15ede887228bf80ef0e0173358a2044b810b795d18eeff9df120ffd5c04ba317fc1746c8d95e1bd809b22871c21963c1f921311d031a009a9e5c3cde07c1701e830ffb32233c7ee5680272ec939f0fc7ae61b8d2294983287d24bc44ba0aace5ca7062b1528ed9def5bb104807c2b80eb1aeed96baa383577bb2ec15476b481908b4f7185a6972b691bab4898b3210c805bb39377ea30f79117689eb51a66c56ffad0acc0ed2dd186ce9352be86a9e5fb80a197444a8e7c86294df8544df18541129892586c98c1b55b01939031705880a080943209be4a7bf268c486aeab9b691ed22645847d0b5dad196849e58ae00e72678022b905cebe4c24caa572cef197bde1093f162d6f55fef74db6b3a9e1e354336280bd7a2ff39c72a2d679c7e6b3fb4d456205b12cea3eba4a6ba0773c065a7d17e28088142a4329b60b63e677026b101791c9e8136c8e4565c4f1bfefa7de32e1dd7c802dafea46d57f96a8cec8423a1df76dd1b17d8c84d8b8bb83f5d4dd1c2a538e7080dc9d1a925493871a66caa78ebc5aaaf38de9037166ecfee4ee5ad4487efae2ea1501808001801f717a9ae03d83cd7d7c4fa88ed24cca2f3f3561778a9f4785ae411765c2b1ea80e1fde57e28948460fb128cd9e77f07a5a6d39bca238d2c5eeced302bc9586450a102808825802b9b6c6f900ebf7b6691c3d4b1613ef0c60bc30cf9f0223f6bb7f89e401300a4804192145190e1eceeaa03841737caddbb50225426bcc9362eb5a71a3ca91c7bce8093617f28e050965a14e7f7a625faf4a2fc1887d468374e9be51b794c0adfd8b780c66c224a560e54335f396386b7b2d5882db320d15225754706fdbd3bd783444c80d03a137efa8979b33767407cea43e871038cbeb9d25942d957a209cbbb33e67db10480a2d780ae96d614a7991566b6e9299a42b9002b776fee14d0caf83522c13f0c0547eb8f80507e03c70a76152a91e714bd22ede0bea0565d2f7d104d323a171167240f729780db5cc927a4c79724dce9f6b30fd57cbe19becd90fe844d7ce848a92e9576cb0b80beea97e151913a98c26931b45537418679e4bd75eb57d09cfe427b6f3cb0eeab80aaee6d0dbe70f31f77a107cd64e3352d6790a0c5611cba00a4ddd4b3c0bfd85b80a094cf65939a7dbf9ea58a5f50ffab443a8e1066dcd4fa0fe200ae11465ba9e58011168bb1e8eabd57367a2ac8a0ee91a5bd3effa0ee4d8e06f260273fb75a679a8016a9c14b30369fc5362586014a7a81de268e624c4a88b6b6adcac8d65d49d0d580430f01d87de62ef0d1eeb196495416de1a78a0a106067fad4e47638c16ac588a350580ba7380c734b9ca8d084822462302c820cef3f9f05148325619a148e94e6a29fc8fa3b380432e1ab06909a56b1d145e0116c89fb2d271142aa3a14d10aeaa9f92cafae0f08040d06a2d458bdb9ebefe5324a87267ee6872279b3836d1b94a8bf2b57eb766cb80fc838b4bfed5801e938b24b9cc3e4926278fde90fcbe3b76cf5d6029a744d99680712626dd7dc266b32cb405d95614721ab58e6ca4011c9a75f485ef336264a161804a6a6e0c1d09d5ab86d07676ca1eb9bf019db220389d9d2062b3b0506befd791803fa41cfeaa690a1905a1b8344b614411bd95d794c9a1c1e7a7674acd01101cbe806c8df455383fa38aa9b43039d479dfb5500100954baa08e76dd779433f819cdd80ace4c5add4286e123c3283928a3fe33a09b73482a653e3556e78560d9a4a82a880516c3a8ddd2044b0d55e2ec72a3368b75ce2f3a04ae2a92b646106f550476576350580babc80f017121ee359a0fbc9506c2a121eb5c07646b5bfb529906d6ba480096c1a21be8032684ebbb5cb0cc89f3f5cfad325460b2cd082a129456b37bb7b714f0ba7a7d680bf50ed835f9476f6dbeb4e834f96530a2966823a4f05513e8d0e510703e0ae3b80221a9f80978b0dbdba1fd06a7e1c51ca9d0e6577e891ed0429a644bd2715aeb58083a66096737828061180a889cfe70b935a9cbc837c58785e9417d1b9acabb11580f447e5fa408585e294fe1f362632dc7406d13ebac7bb434167e3058897009d4f803bffe2c5ac1da2d01550fb43503203a4e6725188e4fd9ac416b481658472c1dd80ad04a231334b570f01da8b886c923e87800f91520abe524305442b7d84da8cfb80e9d23e17d7697f3f539350707c52a09eac89e3fdca28ccf5d4af7bb98161b551802f405366e4ddfdcab5a0050ad150f3b5e75836e152dfbdd6be6c9186a121385b150180c00080b204e3e42df477fb58cbfc87b1fb551fcf739727ff9a6505f8abf8e99d7cd56f807501e5f5f8fd3b8f630d0803bf31c5165489fc5535f61dadeeb0265a846e1d5c350580d2f5800682e06d83bc0cd3056575e63cb629ab09177ea3c18e1a12cc650360f5a571f480aa677cf870990639e59648fa336104953891e123de59a7ff7fa00f1f7d9a54f9803afc779ac5f2ecc35f68a3c2fe6358dbe38c2b0a1a334e830590b2ff8d62cce5800999059af1b3ca2f05027795517bd8b3b525ac116dbe0179d9ee4443b89fafac80d9d525ba97ee1b7ab06320165390c3c63ab97e2cc811683cd339b071ff28290480bb1d1e9225f42a9ec11b3251142027d55febd0ff4a1104e5f51253ada9412ce1804d197c102345549a167b823e5231bfab500b324da1d58562eeef123f248c1fb68028f45cf9e4af5dab186826680f09655dc9caf3bce2daa138b182507b5b2121538027bc4c02653e33d1d156f4d9659d61c9ae60aace29f4650cdea8e6355930ce9a8033d5c2ce050905426bc797be346f498b1d1acffba3989e3960936e7ce5df9617b90580ddda803aedc4a1b20a730c853bec678470102feedace5043aebf780db5d31be472f3cc807905226d0add5f957aabeb90c384389126377090748738717a67e232e213c6c380250d8afc00f148c62049f27eb9842be906b649da1ee879077f170638ec689b988099ebd9d3697150b0e8b39e1850e5b278e62975fa61342f38b1032a4d234f341c806d7ae498ba5a21d7420d784592912c1a9a8af68943b5105860416dccfc8e913c8012c0455f0e4cee303982929199a5cd2173780c8ff78af95fb23dae6aaa7f1c6f8057ec5dc8cfb2c38201e115db779ff0234557c5e4b28fe68b4983fd4f5273380e80662a69c688415b65dc524d10e921e59d2008affbb69e4d889ea9b161e87254ee805953566df20b88fa10fba2a337e20b1af93401dfd6d70bc8ed7f9970189c59fb80220d526eebd17ebde6b34a4556d416338f626d774606c1b932067d67e8fdfdaf80053929f6e362bc1aa0e166e8ef4a02700b5829418d33ae4d2886e27f40a416a1250380e089807d15ea5039b964b37b7d540bdcd7d21c1f704aaca136ea7a0d8a265e30426187804ddcf2a7867a6610d6089cf28f0383d409eb982e9b3ac0e4fd03a53da186aca6805022fe301d5ce92c31ccc69b03d2523b92dbea76846f7b57e3f8f019010cf29480aef8fb173daced83154f619c5b8d4e56be1159fb706a06519a7260b8dccb9946800e605f0b01752038d8233ee5e0fbb7fbd904fd48ea5a7af83710a9628e5becf9808d2a4e3a06f37e6692078cabb1bb38a39e792cb2a012115a2ef89a24eaac311c350580e87b807d48ea0402c7d641f8665742e06d6988c94fa4fa8d49df93fda670b8fd604b508087c8d74cd362222462b1f56a6e2b4dd5447c541f325594c57841b82adc7ef4bd807455e169ad5506a9964aebb958ca522d0037a53e15dd4c24e5da79d18b97015b8066798aee7b53d80ac9f6c138c65108a68c49120d033e77c69399bf8c1055518c80ceb01d9d25e4c29fb63a457b405a13513baad1568e47f37c0156208bb91ad07b8087fbe40264c90d335f7833020fc24ce9403ef3a9322339c8e69957cf0c4eafd880a2ff89620ef4bbd249ef428ae75d8b0acd6f14bbefba925f282c62cb86f3df8180e61c3a046c71dba706af20a6ed8718d4a76a7e5c01f0bf96d0ff22469211511480aa064ed1878e27ebf88ed7fa51042e29f3e812bbcf798ce148fa5b9c9c554adf805dd9eeb0a3092f22399765b46b5531a8d8362e70d78698b2660dd893c9a1b0dfb90580fe3980624c6ca1a33c20f7223d9b74fd230fd59fb04d15620104b78b2c606ad13459668063c5ef0c4d2fc92f2a258ba33981b4bff5d7aab7a0ab22cd69ca7b21c9716edf8060031ba05ece96915b54dd1fe391178833e49c94c0a42744527fbd5bc635fbd38068986af65431982ba9e4eb4a8faa8d1920add57bdb2e8aacbee7e9b6f7da5d66809bbf22b25cb33c0171e1ba444911aeba853bfacfb164d8fca74fd26aac5e609b80aa406c52a8b1a7ed746e4db4f1fc2cdcd04de60dbfbfa6c190d906cdf72f460080b2623df2d9ef0dd77ebe89fbf5a82130274f64c0dad9bbad0af38fee0bf2e89b805dee8d6d9ee79695ec52b69efd1e7fbd71ee104312edf0fb4e34054ac6450c8d804ab922fbaffe74c0080c9647522bdd699d8bc1f2a47d004b91bc33b41744aa61806a70083dbd06317f7a10a6b8af70424efd7d2551f4d27fbe93e0bfcc31ee67328007191bc3a422a9eaa8d8e180b555a997465793b6df1ece9a3b23c6065823fdd9c10680fff280cfd9b2e8b2ea273456701a40665f553c23e7ee3dec27f90bcde2e30f9e26f0cf80eb84715e40c235aa6633288c9293de2b8b2998d77c2d1709f1153a985a520fb1800f68262083d6cf056d04c462655b4bd966ec0c8fedecf5021ea3ec9c25765f7a80a9e1e296ebdfabcbec8d0aba7523aeea9c2520e6cabc85abc82cfbf1f44dac5c80a69c9234da28817aba76a7aefea2df699a8d31f76fba1d4c7fda7a0baba55c6b8015bca333f5129f70c50f38f44bef10e6193d8c9c549ea458359b988aa91c047180d1e8d96a057e6a39cce9679410bc5a6d8c6324d449b5e465601ca571f98b2abd80b972a0002f267ad64a7f55e999c46dafed486f0ea984a60cb1946ee9c66ca32a808acd50d560c8477565e98de81738dd9b56a2d0b2a95f874328a01458fc4f0187804ebe8727fe5db663ecb37497784929245f2f2f92efa96f6f5283be6533529701801fa4a56ab7d16f199f00bcb8741c2b9a99a429414b5b9f513c9b1bb9706952ea805947d2fc36c2c3eed6cd8ca1ad7abf10903483c6651f889948c62515826fc228807e5270d8a27e55d54ef78c09eb9979948100281088aa5f2de813419e3de63e094d0880ffff800b0ec71a4e57828b9f52da46c6bc170be8d1896d6ce7d2721c2f9ce02e5b0a77800050ca5e16fb3aea575fcac11323bb8525e856f6ccf2efd18b666b1cf3f5d71a805315e29e936b9b4db21d5c9a0c0b51219a6ec029d7e8663b062d29f65a63cda080291158b56ad4eb8e687779fc1dc90f131bb17dbdb7ce8ad1ac558f60f47e96cf8038748d5d019786b6148e11b3df6a20486205fe71afdcd8b544ce1dfc689ba6e680115df690f69a5a49eb25ab727c3dccfa352ae906df22e58a5a9e1555e722a9bb806c8e060cf64da0bb40ae104f1fb39056ede02caed68d175a3bd12f1ddf10ac3c80865f92ca93477f25ca0014d71734f46e297220f98e1b25a71dfdc977081a37ac806f16e2a42e12f764b6c4af022eb096ea8999a931e6275e39a0c8e27105a688d180a755d7f11bfc497d25bf89a20b6daf37ed3d3135cefdb1243f479c14eefb91ef802fcbf219df19137025a05e8e168dbc4b6927681d3ef2724bdfd9787f709a233e80a25de7cdc4c8bbfb3d49e1b295c56babcaee6174110482441aeb0151dbb6707d80b1b9bf8ba450e67502fa622e301296407e2b477a351fdc8c09c0f4ef2fb6afd180c0a622f1aef12b2877c9faa6b0b29f693dffc67ee05b09a3d448783a40c21e0680276ed60edbeece7f58dce29f9d4cba7971be961324dc580263f43b87eab9febc80c92ffbbb8d0f2d1bbbeab1c7356ef566113300c71d0c78903b05f8134e30554319018104088080b4b931be80df40abfca20f7db34a14a507e55bd9fd6e8030d0181afb6b72abe480d84d6cc24bce41fda6307fefb84cbc8a1bdf95461520be672b12322f45bc2bd619018104090080904d8e2b22b873e05fb7515c645c262d7693373228858aa6df5a84c70c5226f6809606c00f5eb82f055fe656111690847a4a3241daf61181748e70af6d46eabb3f19018109400180338eff6a6dfc0b20eb1704ff49572980e82320e237fe150fc9c55af75e10e79b80e609fe30ba566b9b263249fba383d73212a641d51c3e36bef38f8f421d7c0a289903910388af62e668997722826b847423c55fa662bc3e8e8e879bb70ec9601e7ddc368afe099f01943ada41dfe46d6fc6fae3af26ab713ce32764bf8b74ec7a79a067e5ff8d041d5731a40e6e865073580f5573124e33dd1fd527d02bf651fdd7103895acb374b60c0661757261206f3f9008000000000452505352906930032dd33b0d421cf40d62865c3ff214198418450934c0e14e882cdb1b60d02e4b4805056175726101010083276d51990521178f1beccbaba213ff8d9821ec4a3184ebeff14d8ad315452cc820fafdd446946ae71ae0ec4c6dd7257a51b660752a5b4619e77ad91e2988bd029d007f03cfdce586301014700e2c2593d1c080aae04d860dccb9347ab4dd069523d44e4082c333ea8d9dc1041e2b06dec33259505f0e7b9012096b41c4eb3aaf947f6ea429080100685f0d9ef3b78afddab7f5c7142131132ad4200200000000000000585f02275f64c354954352b71eea39cfaca210020000004c5f0ec2d17a76153ff51817f12d9cfc3c7f04008037697e3b04c90d3835f568abd4237b003e468e22dc8190549d918ab6ac824d46ed049d0da05ca59913bc38a8630590f2627c07dd80bead730f8ec5a062a67bfad0cdd0648e742bf62e1ded56c9178903277c881d8980ae279b6263f84eae18194845a7936749b56dea832921fc271b062f3b1a4cc73f80ba53f0c1bd9d537531af0897bc1acaa4248f898a8fbd1ec273c3377ebf7d78cd803450827e78d1dd1b9616c1fba9a4bf11a3d6391100362056f157654aab455f7f8007a7e7f67e493ae8db721307a496ac46aef38374c2428ef9b54c056e9e52d6518098f86751ac4a6da04044704a76561dc74fc18aa4ec4207576a302b8b1ea1c83c803a27bb084f90dbfe6c0051a7ce58bb062f03275d537520e5c4675bf66896a6bc8084dc7b5bd82bd8b8a4b76c3465e49a8bc7ebbdda681910c651ef2d2fbc0c2ada80dd3ced4bb463311afb74382bb9af57a8ccf12b6f7606c21307571285b3c37680c1059e710b30bd2eab0352ddcc26417aa1945fd380bdaf2f6099522c1c77073cf1fab0cdd0e095510a1f66f6f5a4564a362699f71b80b77e0bbeacbce266e1b430b877064a5791a00a505f4a7fe3d87425ff8b41265d803cdd80e6bdd3f1f7500913d84c36bb58af34446fa9837725d6b4e54b795f384580952a3a6b440791ec0568ebac76e1daabdd7390900f370b2ae3d23e2ce5fce12480b9aee043e378f8313e68a6030679ccf3880fa1e7ab19b6244b5c262b7a152f004c5f03c716fb8fff3de61a883bb76adb34a2040080830645bd4192ebda0ff5ee2566ffb7d1255542bf848b7f3b6b4d44a27f1b2cd48008a0c609ab4888f02c2545c002153297c2641c5a7b4f3d8e25c634e721f80bea80b6617c764df278313c426c46961ccde8ee7a03f9007b74bc8bc6c49d1583cf7d80396738bb3650276db898dd327b385c19d3ed7364ff3cf71de05dcb99441b6aa2802e637b41d54f85a704cae737990c75ee6dba846432d68f22c8349d799ebd5d34c89e7bbb460270642b5bcaf032ea04d56a40803c570e28b8ad2696ed532c08000004003c570d3c4eac5d2f7717dd07000004007d059eb6f36e027abb2091cfb5110ab5087ff96e685f06155b3cd9a8c9e5e9a23fd5dc13a5ed20e47e201100000000685f08316cbf8fa0da822a20ac1c55bf1be3203a24000000000000505f0e7b9012096b41c4eb3aaf947f6ea429080000809f6044d476f5c87cfa49560b3cd54d74a9d77514ddd82cbe30156a574ccb77f080ccd7eea271ab4ba95eed0fbf898a82fb654e2a3232ed1234bcaa4ab30e8e91ec8036e1131b64486ed89ac2f4677994f96862b3bbed798adab6d65c42e3f31c1817800d49fef039517cc312c00412803ca1df50ac6d90c50541f649a9c85b83c0fdd8805cff934c271c86e14752ba8a87136d13a5ad5f1bcc76a26056bc3627f9b3d523804973f16c6adc2dc9ec1c2097f11e7d4fa868fe855c7bd4aab8b524f71fb0f1de801f3323a69058605fd27a41e4022d453e473c9e30960ba4de7e914036714d67d9685f090e2fbf2d792cb324bffa9427fe1f0e20e00852014012520171019ede3d8a54d27e44a9d5ce189618f22d3008505f0e7b9012096b41c4eb3aaf947f6ea429080b004c5f03b4123b2e186e07fb7bad5dda5f55c0040080e8b9d9fc82b1c6da95995e28c8d999a677bae801565a79a112b54b0f14fa1618d5019ef78c98723ddc9073523ef3beefda0c104480a24170e86def97562ef1e9e5a1ebad73bb5bb4ea1c47806af7d04b65dc6bafe980bb76a3db64dfd07c54fe58ba9dbd08a414502eecab0e38264d24c5f1d3fb79fd807fafbe480fd754f61a15df5768b1e8576d5c887a9921b2ecad63b3f65c628b27c1079f012b746dcf32e843354583c9702cc020ffbf80dbe1e1a78164149c060e2a58b087d09ccf015a3c31adfc7a4632e6a7c07c4cbc80abfb119d2bd8cc9bdd0d0797556f084d13abb69af8080e05efe1a67130fc3bce809f650307b13e3a101f060f1cf03962836d5b14e1b85bca22b9e4774031fc310480c435a115fde448f8c6f3e1e160f987223d5958aa2c8cdac2758b6fa06766fd885c570e26472d4991124a290d00002408e8030000f207000080d4da56980b78ca2be50ca171be7d145de314f27b43b40b55bf536a5cba92621180434ba644f3455be4a500c35d3d856f5530254764283cfbf6d4a512a7d10bc7315c5700bd9a93e85e3ce1d20700002408d6070000dc07000080740883c6d9e7b76273df084d7ad315ce5e7b72a3c2ba7d6f4bca40cb2f9949a4802cd9c92041bf3b9302e743b05418831fb268bbf92920fd03e259bf52fbc7c4238014ab60b82a2bdd4969b94f9326278eb925d8065d1f8ec49f5ef8565f23be5546800dd5e02800bdcc2b2601ffddc3612f3692afbf8cd0fac750b7ec5deca41ea39080477cdd86e95df4b4c9618e1d03638353a9dd312d16ec83730a2d29c79111d5f68019e9c2e1de5338b5c164ebba324dfe1526d319330b1b6dfb868ff03d770c7c6b80239130238d35af424219fe20e161c55d520d9cf94e0998aead2eed05f7b075c78d089f06604cff828a6e3f579ca6c59ace013dffff80a67df049de327ca7280faff9a23fa6e899eb43e3bc5bffc68e669da46f4a009380d7209ba49d1b603b14ca986fe4c3f1eb10821a392318adec37b41d32698be95f807afc16c2cc8659335d2d12ddf49add7b8ecef93384bfcc21f00711f0e340cdf9802ade1f39a587618d505435d78427ad95f0baba15d37717fa854cf482ceb23559809c716bd290101e8b06930233193df55f25ffdea6ade39620470b074bd97d56f480e95377cfb4d05711c0ea3ad835ee78c4b06b21588b1b056640d32506cd7c8b26805fe8256614747b510cfee87385f44249fb7f66757536605473bec5029d3b2d90808e937ec910e9f191f28d2fc97f607b2851043c81bc61eae574e5c010150e9b5180f93abde928bfa936f9e3d37efff87364bac10e3fd2323ed209d266a1cfafb05580e179e2a9ef590be7e088857e1e5faab391a3b4db3775c0b836e773321af4df07804dc96489b16b499113d4ee38a995691d81d071f760e2bf6d061576930947df9180a04035516ef854ec8ec48892629da0b2591fc11c7e43631c14b9ec1a5a9692f580753a5b673550e341885c4df44767005826701ea516ac1204e63489d2489d7d8780188f3e7cacb48a98a152ee02ca085accd25ba6440f914a2e80769dcaee5f8721803e8ef9ed5a2b09820e790c96b53f7779daff6bd31774e96414f53abdda424e898078ec216dcd2a5355a091ae15f3e6e09ff3d6f46275e54870e449bf359324760b35079f0ad157e461d71fd4c1f936839a5f1f3e7fbf801c145cc01e958be57dbedca18f3fac8a3a94edd9acd6ef14f97e98be00d7e0445857060394315fe95b98f3070000200000000000000000803c0587a558258baa384d04528923ca513d49c75463147d91968503f3e81b041b80719b1a00d76f60c8fb60e7b4b732dc836fd896cc23266cb6143a8c1c6dc0274e58570e26472d4991124a290d000020000000000000000080dd201692e326905da172c3b0545df08d5b8b71f950e592fc027e4845cedfbf1a80301a4f587cf9c675cdd69154e7092c8aa6b66146f4aea52f231cea9bd4937e7080e0bc9102a86834b5ef2029eb4c635c7d14ec8347288d2d28450594ccf555f2d580b0e496325bd904415d8e1bc9607bb0a64756433cfb52007d1e36aca9bf725f38809cdc29e8e561745cfc7524d3e54f6276e075045e616c3b7e5029c190d2ab3adb80faf7461a587742f795647832bbd5e6ef3100341d3ccc7d302fa748b194a81faf80db9c2cc6a5450b2c2840f51f7666e1d6f3f67fdea291c6270e2388360500651480d4fe3e57123722d2122f49971109b3658b13ae721b425adf35e278e6ddd4bb8c804793443a067df5e20bbe8022ed078cea4d714ade8d279591ed29d8a9dc7543e28d089f0b3c252fcb29d88eff4f3de5de4476c3ffff801766997c2c80d0d492b38b70c2cd9ac5b99b31c564f3744285b0fe34d5c8762580d9e921dc81ebafaeb17be6a3025644721106b167e2a7150f4b8fd93661cb2eda80d4ed5ec96adc7c0f5fe25d57bb11b50fb6a857960ebd5187592efb5efc12f44280bc257bcbb3124d1cf4f03656136bf06949834e51840697e963d44153dd9d68f18043f39ae8dce3d31d7d72c3390646770721e1b02a15565feccfebb10e5d6c640d805778b36d64ef1c0c52d312132c667c88c8e551a94b36beb6f3945ed6182b8f46802505e8361d83ee8f129857fd60e9f7ee7b96c9dc94832a095690a5e4a928eb2880788bb0b51d78020e6ce759b5e1fd646d668ebc848ac61c33005625ea93cd8a8080035806cd6af55b10d85177bc6d711de6d3ee6928457ad63cfd6b5f7378711b5d8033e7559ca8da82a74a05655643de1c14247207ee05396c0d6b032a15ab11fd2480b74f6a740b495c0b36e5499d26fa5465a2b13a28b618135153b5fe09358e4e18801599f7348acd400047f4d3fe1c6816db8a73f346e3d4ea3f82a0bb4ccac126d38070f1f3de0f301b5619d5df2094803c3e745996df63fea760effbbacbbcb526708011967b9b08f3dbf29a40cc854a45c54420875ced766d7f3fa3ae7979804003028077527b37043eb312f01f8dbd7b787cd52e6ab7309c22b9d0726acbe7b8238896800505fe26d87c0e102c6a32d8c82fc6894a97ca2e992c423215f5c385a3eb3160c1079f0d3719f5b0b12c7105c073c507445948ffbf8082517fbaa87d47a17df723384811b7f430e8b4cb5fb8b52e4006437514ecd52280abfb119d2bd8cc9bdd0d0797556f084d13abb69af8080e05efe1a67130fc3bce809f650307b13e3a101f060f1cf03962836d5b14e1b85bca22b9e4774031fc310480d728b034475ebdeb21ec9ccd4b0789d9de067199335518ee818689a8e12a416e5c570e26472d4991124a290d00002408e8030000f207000080d4da56980b78ca2be50ca171be7d145de314f27b43b40b55bf536a5cba92621180434ba644f3455be4a500c35d3d856f5530254764283cfbf6d4a512a7d10bc7315c5700bd9a93e85e3ce1d20700002408d6070000dc07000080740883c6d9e7b76273df084d7ad315ce5e7b72a3c2ba7d6f4bca40cb2f9949a4802cd9c92041bf3b9302e743b05418831fb268bbf92920fd03e259bf52fbc7c4238014ab60b82a2bdd4969b94f9326278eb925d8065d1f8ec49f5ef8565f23be5546800def162e19243ae8ca7c44d4c468ab8cbb734bab24b8ce7abd1e1e5bb89d021680477cdd86e95df4b4c9618e1d03638353a9dd312d16ec83730a2d29c79111d5f68094f696b346b1988edff6a9eda556f529d7f9fcddd6648b18e27c624c7a38ae8080239130238d35af424219fe20e161c55d520d9cf94e0998aead2eed05f7b075c709089f0d7fefc408aac59dbfe80a72ac8e3ce5ffbf80fc6820f2f11b2e1106ddd8093289def88c16a1a666e5a7ca274f5b9ad1205c348024bcaded99b7b662feadced2145fddc5017b64258c17f9c1470e0692516c1748808ab1425e43f1cb6c98c7e99c01e465e2dd0db98a42367da44598e48c5384785a80d6fccdfe48431d4dd932d2109397b65ce446d78e2b010320fa102a93b61e2a4c8036f44ea66066d141e41610fbf6e15f836844c6c0a425624d9404016c584e632e800520e0e2172712642c3ff4e226e142dd510fa5bc5390cba2d9a36757ed0074ab801daf4da9b90299d68a8356402dabe09e78840bc175c30297bc54e5122f1d985180a9e6d29bd1ee69601d4b3bc1f545c816c3d0508772b487f6b2a3d8d3e14fadd7802f77b64e6d325844c51b8e2ecc6f29daa0c3fc3c4a78f470ce7ca6401a50be6d80c1787497baae3935d4405ffd8a606a7b377a28c4c2a6798693d09e98fe8e195e80cc9ecbfbb03423f4f4ef48bf85d936a35abf50f7eba0db0d38eda247a748f10980bc4fd2ac945977c6531d7075fffcdd804e4abf46180da28da9e46f01cec6872e80b9a75b4a4de791b555074784a3c0ac8aa0580c7af4a01301f91f8f2698a8ba6e80e635069e2d8a7fec91e43f89c4ea7392a8e8bdb623a8194b0869b6ef5ac48e2180e4184ae23f84d73738eda59f52442fa3567a086cd7a742d10d8f5f5927a415e54504bf0e02656c61795f64697370617463685f71756575655f72656d61696e696e675f63617061636974790fe080792dd057260f9396f4a568f8af05f29fda85685cd206c5e43696a10476c49b328028332fe594248146bfe1a7fa978e28b99859038bbddb5cc563a7c771ddfdb395809672128c947bb3ad8db86ecf6c3447834d5362d0e50d94f6448fbc25f60bd55a7c802001344608000020aaaa020000001000344608000020aaaa02000000100080fef78087f3f979878372707cbf223b2cb5f9ff6829eec0ffe7e965d5b458dba480010bed059effe5feb8c7e81ce843f6b34ef8e42d0eb56b7001d26d05525574f980c1c5d7cd0aff0514d7affea47aff45e00929fdeffcd7bcd00f573c585559c1e55501e8030000009001000090010000000000000000000117bd272b0646c5d00cba5dfa84f4bcf40affc6e0901c0e116b3a679aeec62bab00e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000011ef02c307fe2458fdc3421ff6437f07825df9576ca5fa3f7aa7efe55b9e6a3ad00e8764817000000000000000000000000e876481700000000000000000000005501e8030000009001000090010000000000000000000120a12008f72dbe2670def861ebaa47a7e8eb9f9eb189a9a2a9a5d708fa686ee100e8764817000000000000000000000000e876481700000000000000000000005501e8030000009001000090010000000000000000000132fb65c7e69bbc31b59528e26486176b011c5b2faa68149eaaa815497169a77200e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000013668753fd6b80f88236e7c79669b7e7eb474987eee5370569d59b44fcda6584c00e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001366ce3642662fc61178f66af2ea3780d9e81cacab9145532eadc0233be4a653400e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000013838b16fabe899097d62c2c4eee427d29cf317c0313722140a83a0f1b2cf800600e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000014354afbeaf4ae64390b5c43c1b8d0cbf40fe68d16f94d93f591bfee2b9cf906c00e8764817000000000000000000000000e876481700000000000000000000005501e8030000009001000090010000000000000000000153cce85c7c975bdaa23be6fafc0e4c37bc5fa4286651f462b243ed8cdd78669d00e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000016305e7858e7c23ef636e327f2e9959e74c96106aa3f9d9f1891b8536c5eff68100e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001660741b961bcdbef39ce499d4e4d74f3c17fdc2e3e7d89ba07c3dff957be137000e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000016ed1fa87208a8d0d7edf09c160533dfd4d2db5079f6cf014759d5b725d0d575f00e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001767e1e8b317dba20958be4019d2e653b7cccfedd1073317d2f0b5835d521ea2b00e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000017c2d287d53414a6b6e6feee9c5629856198d9950b3c46cf5c7eb1bc1774b324d00e8764817000000000000000000000000e876481700000000000000000000005501e8030000009001000090010000000000000000000181750c66e01852fe21046e96ad3c420c7fbcb5c3ec7e8c065286eda7ba967bb200e8764817000000000000000000000000e876481700000000000000000000005501e8030000009001000090010000000000000000000189cfcc5dc2cad65e2c74cf0622bd0ba735677bfac1df504efddb5a0afbb2bf2000e8764817000000000000000000000000e876481700000000000000000000005501e803000000900100009001000000000000000000019df43ca1ed4d3552f8a7f088f00a0b30f3d929c18ab41d92896b101cf70cba1300e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001a5fb9adb4155b79d167367294aad54e4a93a74af244e0be182bbfa8cf1ea1eec00e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001ac705b7a0dcdc8ed3971e11255bb259942a0e837a6e44562b7118cc2f618a38000e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001b652a9157226ae538b65d450b28f7bb3ff958fce793e7bca00b18411e7689e7f00e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001d2da2de1b3a15578bdd33a308579ae74d6cf2f3260d10d8cb9a40ae32cc030c100e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001d6ff654d9d900cf1509af8de7c9d87869b12a1d4750d70ad831acac104185c6200e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001dfcfaa7271200ee345bb87440866ab9571e04bec87d87643b6ef1261ad326e8300e8764817000000000000000000000000e876481700000000000000000000005501e80300000090010000900100000000000000000001fcbc16535a988731a06abbcec463b4361bfce207c3d2c72778a6eac1a3e5d1fc00e8764817000000000000000000000000e876481700000000000000000000000030e803000000d407000000d607000000d807000000db07000000dc07000000e907000000f007000000f207000000f307000000f5070000003808000000", + "0xb50184006217046791cb1af6395d39fb87571eef8a59c62bf7e2ea3fa046f117ad077a78017ef00383a3ef3a253fb8ecf00f9087f8c4a95b790b45ed642a25abd2c935693939826364b78df87a7a6dd1281750a3f910064980f9400dd4984e50f3f229568ac4017901007802020000", +] +`; + +exports[`chainHead_v1 rpc > runs runtime calls 1`] = `"0x146163616c61146163616c6101000000ca0800000000000038df6acb689907609b0500000037e397fc7c91f5e40200000040fe3ad401f8959a06000000d2bc9897eed08f1503000000f78b278be53f454c02000000dd718d5cc53262d401000000ab3c0572291feb8b01000000bc9d89904f5b923f0100000037c8bb1350a9a2a8040000006ef953004ba30e5901000000955e168e0cfb3409010000009af86751b70c112d02000000e3df3f2aa8a5cc5702000000ea93e3f16f3d6962020000000300000000"`; diff --git a/packages/e2e/src/build-block.test.ts b/packages/e2e/src/build-block.test.ts index 73c58012..7d5d655b 100644 --- a/packages/e2e/src/build-block.test.ts +++ b/packages/e2e/src/build-block.test.ts @@ -32,14 +32,14 @@ describe.runIf(process.env.CI || process.env.RUN_ALL).each([ { chain: 'Westmint', endpoint: 'wss://westmint-rpc.polkadot.io' }, { chain: 'Westend Collectives', endpoint: 'wss://sys.ibp.network/collectives-westend' }, ])('Latest $chain can build blocks', async ({ endpoint, storage }) => { - const { setup, teardownAll } = await setupAll({ endpoint }) + const { setupPjs, teardownAll } = await setupAll({ endpoint }) afterAll(async () => { await teardownAll() }) it('build blocks', { timeout: 300_000, retry: 1 }, async () => { - const { chain, ws, teardown } = await setup() + const { chain, ws, teardown } = await setupPjs() storage && (await ws.send('dev_setStorage', [storage])) const blockNumber = chain.head.number await ws.send('dev_newBlock', [{ count: 2 }]) @@ -48,7 +48,7 @@ describe.runIf(process.env.CI || process.env.RUN_ALL).each([ }) it('build block using unsafeBlockHeight', async () => { - const { chain, ws, teardown } = await setup() + const { chain, ws, teardown } = await setupPjs() storage && (await ws.send('dev_setStorage', [storage])) const blockNumber = chain.head.number const unsafeBlockHeight = blockNumber + 100 diff --git a/packages/e2e/src/chainHead_v1.test.ts b/packages/e2e/src/chainHead_v1.test.ts new file mode 100644 index 00000000..5cc8943d --- /dev/null +++ b/packages/e2e/src/chainHead_v1.test.ts @@ -0,0 +1,119 @@ +import { RuntimeContext } from '@polkadot-api/observable-client' +import { describe, expect, it } from 'vitest' + +import { dev, env, observe, setupPolkadotApi } from './helper.js' +import { firstValueFrom } from 'rxjs' + +const testApi = await setupPolkadotApi(env.acalaV15) + +describe('chainHead_v1 rpc', () => { + it('reports the chain state', async () => { + const chainHead = testApi.observableClient.chainHead$() + const { next, error, subscription, nextValue } = observe(chainHead.follow$) + + const initialized = await nextValue() + expect(initialized).toMatchSnapshot() + + const blockHash = await dev.newBlock() + + const [[newBlock], [bestBlock], [finalized]] = next.mock.calls.slice(1) + + expect(newBlock).toEqual({ + type: 'newBlock', + blockHash, + parentBlockHash: '0x6c74912ce35793b05980f924c3a4cdf1f96c66b2bedd0c7b7378571e60918145', + newRuntime: null, + }) + expect(bestBlock).toEqual({ + type: 'bestBlockChanged', + bestBlockHash: blockHash, + }) + expect(finalized).toEqual({ + type: 'finalized', + finalizedBlockHashes: [blockHash], + prunedBlockHashes: [], + }) + + expect(error).not.toHaveBeenCalled() + subscription.unsubscribe() + chainHead.unfollow() + }) + + it('resolves storage queries', async () => { + const chainHead = testApi.observableClient.chainHead$() + + const keyEncoder = (addr: string) => (ctx: RuntimeContext) => + ctx.dynamicBuilder.buildStorage('System', 'Account').enc(addr) + const emptyAccount = await firstValueFrom( + chainHead.storage$(null, 'value', keyEncoder('5F98oWfz2r5rcRVnP9VCndg33DAAsky3iuoBSpaPUbgN9AJn')), + ) + + // An empty value resolves to null + expect(emptyAccount).toEqual(null) + + // With an existing value it returns the SCALE-encoded value. + const resultDecoder = (data: string | null, ctx: RuntimeContext) => + data ? ctx.dynamicBuilder.buildStorage('System', 'Account').dec(data) : null + const account = await firstValueFrom( + chainHead.storage$( + null, + 'value', + keyEncoder('2636WSLQhSLPAb4rd7qPgCpSKEjAz6FAbHYPAex6phJLNBfH'), + null, + resultDecoder, + ), + ) + expect(account).toMatchSnapshot() + + chainHead.unfollow() + }) + + it('resolves partial key storage queries', async () => { + const chainHead = testApi.observableClient.chainHead$() + + const receivedItems = await firstValueFrom( + chainHead.storage$(null, 'descendantsValues', (ctx) => + ctx.dynamicBuilder.buildStorage('Tokens', 'TotalIssuance').enc(), + ), + ) + + expect(receivedItems.length).toEqual(26) + + chainHead.unfollow() + }) + + it('resolves the header for a specific block', async () => { + const chainHead = testApi.observableClient.chainHead$() + + const header = await firstValueFrom(chainHead.header$(null)) + + expect(header).toMatchSnapshot() + + chainHead.unfollow() + }) + + it('runs runtime calls', async () => { + const chainHead = testApi.observableClient.chainHead$() + + const result = await firstValueFrom(chainHead.call$(null, 'Core_version', '')) + + expect(result).toMatchSnapshot() + + const nonExisting = firstValueFrom(chainHead.call$(null, 'bruh', '')) + + await expect(nonExisting).rejects.toThrow('Function to start was not found') + + chainHead.unfollow() + }) + + it('retrieves the body for a specific block', async () => { + const chainHead = testApi.observableClient.chainHead$() + + const { hash } = await firstValueFrom(chainHead.finalized$) + const result = await firstValueFrom(chainHead.body$(hash)) + + expect(result).toMatchSnapshot() + + chainHead.unfollow() + }) +}) diff --git a/packages/e2e/src/helper.ts b/packages/e2e/src/helper.ts index 4399198e..0108eec9 100644 --- a/packages/e2e/src/helper.ts +++ b/packages/e2e/src/helper.ts @@ -1,8 +1,12 @@ import { ApiPromise, HttpProvider, WsProvider } from '@polkadot/api' import { HexString } from '@polkadot/util/types' +import { Mock, beforeAll, beforeEach, expect, vi } from 'vitest' +import { Observable } from 'rxjs' import { ProviderInterface } from '@polkadot/rpc-provider/types' import { RegisteredTypes } from '@polkadot/types/types' -import { beforeAll, beforeEach, expect, vi } from 'vitest' +import { SubstrateClient, createClient } from '@polkadot-api/substrate-client' +import { getObservableClient } from '@polkadot-api/observable-client' +import { getWsProvider } from '@polkadot-api/ws-provider/node' import { Api } from '@acala-network/chopsticks' import { Blockchain, BuildBlockMode, StorageValues } from '@acala-network/chopsticks-core' @@ -33,6 +37,11 @@ export const env = { // 3,800,000 blockHash: '0x0df086f32a9c3399f7fa158d3d77a1790830bd309134c5853718141c969299c7' as HexString, }, + acalaV15: { + endpoint: 'wss://acala-rpc.aca-api.network', + // 6,800,000 + blockHash: '0x6c74912ce35793b05980f924c3a4cdf1f96c66b2bedd0c7b7378571e60918145' as HexString, + }, rococo: { endpoint: 'wss://rococo-rpc.polkadot.io', blockHash: '0xd7fef00504decd41d5d2e9a04346f6bc639fd428083e3ca941f636a8f88d456a' as HexString, @@ -66,37 +75,51 @@ export const setupAll = async ({ throw new Error(`Cannot find header for ${blockHash}`) } - return { - async setup() { - blockHash ??= await api.getBlockHash().then((hash) => hash ?? undefined) - if (!blockHash) { - throw new Error('Cannot find block hash') - } + const setup = async () => { + blockHash ??= await api.getBlockHash().then((hash) => hash ?? undefined) + if (!blockHash) { + throw new Error('Cannot find block hash') + } - const chain = new Blockchain({ - api, - buildBlockMode: BuildBlockMode.Manual, - inherentProviders, - header: { - hash: blockHash, - number: Number(header.number), - }, - mockSignatureHost, - allowUnresolvedImports, - registeredTypes, - runtimeLogLevel, - db: !process.env.RUN_TESTS_WITHOUT_DB ? new SqliteDatabase('e2e-tests-db.sqlite') : undefined, - processQueuedMessages, - }) + const chain = new Blockchain({ + api, + buildBlockMode: BuildBlockMode.Manual, + inherentProviders, + header: { + hash: blockHash, + number: Number(header.number), + }, + mockSignatureHost, + allowUnresolvedImports, + registeredTypes, + runtimeLogLevel, + db: !process.env.RUN_TESTS_WITHOUT_DB ? new SqliteDatabase('e2e-tests-db.sqlite') : undefined, + processQueuedMessages, + }) - if (genesis) { - // build 1st block - await chain.newBlock() - } + if (genesis) { + // build 1st block + await chain.newBlock() + } - const { port, close } = await createServer(handler({ chain }), 0) + const { port, close } = await createServer(handler({ chain }), 0) + const ws = new WsProvider(`ws://localhost:${port}`, 3_000, undefined, 300_000) + + return { + chain, + port, + ws, + async teardown() { + await delay(100) + await close() + }, + } + } + + return { + async setupPjs() { + const { chain, ws, teardown } = await setup() - const ws = new WsProvider(`ws://localhost:${port}`, 3_000, undefined, 300_000) const apiPromise = await ApiPromise.create({ provider: ws, noInitWarn: true, @@ -110,8 +133,25 @@ export const setupAll = async ({ api: apiPromise, async teardown() { await apiPromise.disconnect() - await delay(100) - await close() + await teardown() + }, + } + }, + async setupPolkadotApi(): Promise { + const { chain, port, ws, teardown } = await setup() + + const substrateClient = createClient(getWsProvider(`ws://localhost:${port}`)) + const observableClient = getObservableClient(substrateClient) + + return { + chain, + substrateClient, + observableClient, + ws, + async teardown() { + observableClient.destroy() + substrateClient.destroy() + await teardown() }, } }, @@ -122,16 +162,24 @@ export const setupAll = async ({ } } +interface TestPolkadotApi { + ws: WsProvider + chain: Blockchain + substrateClient: SubstrateClient + observableClient: ObservableClient + teardown: () => Promise +} + export let api: ApiPromise export let chain: Blockchain export let ws: WsProvider export const setupApi = (option: SetupOption) => { - let setup: Awaited>['setup'] + let setup: Awaited>['setupPjs'] beforeAll(async () => { const res = await setupAll(option) - setup = res.setup + setup = res.setupPjs return res.teardownAll }) @@ -146,6 +194,35 @@ export const setupApi = (option: SetupOption) => { }) } +type ObservableClient = ReturnType +export const setupPolkadotApi = async (option: SetupOption) => { + let setup: Awaited>['setupPolkadotApi'] + const result = { + chain: null as unknown as Blockchain, + substrateClient: null as unknown as SubstrateClient, + observableClient: null as unknown as ObservableClient, + } + + beforeAll(async () => { + const res = await setupAll(option) + setup = res.setupPolkadotApi + + return res.teardownAll + }) + + beforeEach(async () => { + const res = await setup() + ws = res.ws + chain = result.chain = res.chain + result.substrateClient = res.substrateClient + result.observableClient = res.observableClient + + return res.teardown + }) + + return result +} + export const dev = { newBlock: (param?: { count?: number; to?: number }): Promise => { return ws.send('dev_newBlock', [param]) @@ -181,3 +258,40 @@ export const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve const { check, checkHex, checkSystemEvents } = withExpect(expect) export { defer, check, checkHex, checkSystemEvents } + +export const observe = (observable$: Observable) => { + const next: Mock<[T], void> = vi.fn() + const error: Mock = vi.fn() + const complete: Mock<[], void> = vi.fn() + + const getEmissions = () => next.mock.calls.map((v) => v[0]) + + let resolvePromise: ((value: T) => void) | null = null + let rejectPromise: ((error: any) => void) | null = null + let promise: Promise | null = null + const nextValue = () => + promise ?? + (promise = new Promise((resolve, reject) => { + rejectPromise = reject + resolvePromise = (v) => { + promise = null + resolve(v) + } + })) + + const subscription = observable$.subscribe({ + next: (v) => { + resolvePromise?.(v) + next(v) + }, + error: (e) => { + rejectPromise?.(e) + error(e) + }, + complete: () => { + rejectPromise?.(new Error('Subscription completed without a new value')) + complete() + }, + }) + return { getEmissions, nextValue, next, error, complete, subscription } +} diff --git a/yarn.lock b/yarn.lock index 4c2f5fd7..766b3bcb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -53,7 +53,11 @@ __metadata: dependencies: "@acala-network/chopsticks": "workspace:*" "@acala-network/chopsticks-testing": "workspace:*" + "@polkadot-api/observable-client": "npm:^0.5.3" + "@polkadot-api/substrate-client": "npm:^0.2.1" + "@polkadot-api/ws-provider": "npm:^0.2.0" "@polkadot/api": "npm:^12.3.1" + rxjs: "npm:^7.8.1" typescript: "npm:^5.5.3" vitest: "npm:^1.4.0" languageName: unknown @@ -1261,6 +1265,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^1.4.0": + version: 1.5.0 + resolution: "@noble/hashes@npm:1.5.0" + checksum: 10c0/1b46539695fbfe4477c0822d90c881a04d4fa2921c08c552375b444a48cac9930cb1ee68de0a3c7859e676554d0f3771999716606dc4d8f826e414c11692cdd9 + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -1389,6 +1400,13 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/json-rpc-provider-proxy@npm:0.2.0": + version: 0.2.0 + resolution: "@polkadot-api/json-rpc-provider-proxy@npm:0.2.0" + checksum: 10c0/f8b314e35b14d1b8599ad134246e6c006e5c13aa42d6c2d868c28fa69701becb05f142ce765258061b0320750abbe39654a26ea6b734b5ccb83e0193f59d2697 + languageName: node + linkType: hard + "@polkadot-api/json-rpc-provider@npm:0.0.1": version: 0.0.1 resolution: "@polkadot-api/json-rpc-provider@npm:0.0.1" @@ -1396,6 +1414,13 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/json-rpc-provider@npm:0.0.3": + version: 0.0.3 + resolution: "@polkadot-api/json-rpc-provider@npm:0.0.3" + checksum: 10c0/7c27bf20263fea8d6f0b7c77e2498c535889448c3f65a8100f95f761859db39cd4bfdc6646fa2bda3af345d853a5320695cd3630506792f5a323077964c04399 + languageName: node + linkType: hard + "@polkadot-api/metadata-builders@npm:0.0.1": version: 0.0.1 resolution: "@polkadot-api/metadata-builders@npm:0.0.1" @@ -1406,6 +1431,16 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/metadata-builders@npm:0.7.0": + version: 0.7.0 + resolution: "@polkadot-api/metadata-builders@npm:0.7.0" + dependencies: + "@polkadot-api/substrate-bindings": "npm:0.7.0" + "@polkadot-api/utils": "npm:0.1.1" + checksum: 10c0/0aef58dccd134b6fb03dac4120b71ac40d317a2e12274147de053623590e2de46ac775095cddccabe99af972a7ffecc5d0a846fe14940af2c174eaa71362b4d4 + languageName: node + linkType: hard + "@polkadot-api/observable-client@npm:0.1.0": version: 0.1.0 resolution: "@polkadot-api/observable-client@npm:0.1.0" @@ -1420,6 +1455,20 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/observable-client@npm:^0.5.3": + version: 0.5.3 + resolution: "@polkadot-api/observable-client@npm:0.5.3" + dependencies: + "@polkadot-api/metadata-builders": "npm:0.7.0" + "@polkadot-api/substrate-bindings": "npm:0.7.0" + "@polkadot-api/utils": "npm:0.1.1" + peerDependencies: + "@polkadot-api/substrate-client": 0.2.1 + rxjs: ">=7.8.0" + checksum: 10c0/417b0fb09cf7ce6c9873a57fda3417e733df492b42bab738b47e1c70e4735348655910fcef771a6367cf40b453d3271e8346e1ebbd3338fdef8f328dd0cd45cc + languageName: node + linkType: hard + "@polkadot-api/substrate-bindings@npm:0.0.1": version: 0.0.1 resolution: "@polkadot-api/substrate-bindings@npm:0.0.1" @@ -1432,6 +1481,18 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/substrate-bindings@npm:0.7.0": + version: 0.7.0 + resolution: "@polkadot-api/substrate-bindings@npm:0.7.0" + dependencies: + "@noble/hashes": "npm:^1.4.0" + "@polkadot-api/utils": "npm:0.1.1" + "@scure/base": "npm:^1.1.7" + scale-ts: "npm:^1.6.0" + checksum: 10c0/69e983692a9049af7b5bbfef21c7f3352c153ddd7695f26422b4ee9291320f8d39fad80d0f4b76bdc6ec977df50fc70e4197803ce6d9c0c12a263fa9667e68df + languageName: node + linkType: hard + "@polkadot-api/substrate-client@npm:0.0.1": version: 0.0.1 resolution: "@polkadot-api/substrate-client@npm:0.0.1" @@ -1439,6 +1500,16 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/substrate-client@npm:^0.2.1": + version: 0.2.1 + resolution: "@polkadot-api/substrate-client@npm:0.2.1" + dependencies: + "@polkadot-api/json-rpc-provider": "npm:0.0.3" + "@polkadot-api/utils": "npm:0.1.1" + checksum: 10c0/c057688c926e0a59009508017ff56708d2f58d4a126bd1ce45c677de7e6c38fa7675ec1634602dce606f97df0f97e6f3d61dec504075f4839381ff2dd14cc116 + languageName: node + linkType: hard + "@polkadot-api/utils@npm:0.0.1": version: 0.0.1 resolution: "@polkadot-api/utils@npm:0.0.1" @@ -1446,6 +1517,24 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/utils@npm:0.1.1": + version: 0.1.1 + resolution: "@polkadot-api/utils@npm:0.1.1" + checksum: 10c0/25e4da0e2defb713d18cd0c0db594a89cc4e23f36b2ebc5bccb1e2a8ba9a9814d09630d577b98ebcfdbbda2861fa8be48e914bf5f461481f3a09f1627ea6e784 + languageName: node + linkType: hard + +"@polkadot-api/ws-provider@npm:^0.2.0": + version: 0.2.0 + resolution: "@polkadot-api/ws-provider@npm:0.2.0" + dependencies: + "@polkadot-api/json-rpc-provider": "npm:0.0.3" + "@polkadot-api/json-rpc-provider-proxy": "npm:0.2.0" + ws: "npm:^8.18.0" + checksum: 10c0/502dab5f7888a895b990d954a6b0327e318d34f07b04b38c04cc4e2db6022f9b46fed546ead6b21b13946e07ce7e833d427e173fcfe3aa7b0be2b189bcb2a693 + languageName: node + linkType: hard + "@polkadot/api-augment@npm:12.3.1, @polkadot/api-augment@npm:^12.3.1": version: 12.3.1 resolution: "@polkadot/api-augment@npm:12.3.1" @@ -1991,7 +2080,7 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5": +"@scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5, @scure/base@npm:^1.1.7": version: 1.1.7 resolution: "@scure/base@npm:1.1.7" checksum: 10c0/2d06aaf39e6de4b9640eb40d2e5419176ebfe911597856dcbf3bc6209277ddb83f4b4b02cb1fd1208f819654268ec083da68111d3530bbde07bae913e2fc2e5d @@ -10068,7 +10157,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.8.1": +"ws@npm:^8.16.0, ws@npm:^8.17.1, ws@npm:^8.18.0, ws@npm:^8.8.1": version: 8.18.0 resolution: "ws@npm:8.18.0" peerDependencies: