diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 6484c0c..07b7f9d 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/docs/openapi.yaml b/docs/openapi.yaml index dd154ae..813bd16 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -99,16 +99,12 @@ components: description: Tx shortened metadata on hex format TxPayload: required: - - callData - - signedExtensions + - txBlob - chainConfig properties: - callData: + txBlob: type: string description: Transaction payload to be signed, in serialized format - signedExtensions: - type: string - description: Signed extensions as complementary data required to the signing process, in serialized format chain: type: object properties: diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 0b68aca..275b767 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -89,11 +89,13 @@ dependencies = [ name = "get-metadata" version = "0.1.0" dependencies = [ + "array-bytes", "frame-metadata", "hex", "merkleized-metadata", "neon", "parity-scale-codec", + "scale-decode", "scale-info", ] @@ -166,7 +168,7 @@ checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "merkleized-metadata" version = "0.1.0" -source = "git+https://github.com/Zondax/merkleized-metadata?rev=7bfc09349f6ca29e1950644116d786a3aa140a40#7bfc09349f6ca29e1950644116d786a3aa140a40" +source = "git+https://github.com/Zondax/merkleized-metadata?rev=cd1363a2c4702abf34fcc461055f0059b3c32bec#cd1363a2c4702abf34fcc461055f0059b3c32bec" dependencies = [ "array-bytes", "blake3", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 230f41e..8a54be1 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -16,7 +16,9 @@ frame-metadata = { version = "16.0.0", default-features = false, features = [ "decode", ] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } -merkleized-metadata = { git = "https://github.com/Zondax/merkleized-metadata", default-features = false, rev = "7bfc09349f6ca29e1950644116d786a3aa140a40" } +merkleized-metadata = { git = "https://github.com/Zondax/merkleized-metadata", default-features = false, rev = "cd1363a2c4702abf34fcc461055f0059b3c32bec" } neon = "1.0.0" parity-scale-codec = { version = "3.6.9", default-features = false } scale-info = { version = "2.11.1", default-features = false } +array-bytes = { version = "6.2.2", default-features = false } +scale-decode = { version = "0.12.0", default-features = false } \ No newline at end of file diff --git a/rust/index.d.ts b/rust/index.d.ts index 12fe6c3..b70a1f5 100644 --- a/rust/index.d.ts +++ b/rust/index.d.ts @@ -1,14 +1,19 @@ -import { ChainProps } from '../src/types' +import { ChainProps } from "../src/types"; export interface RootHashParams { - metadata: string - props: ChainProps + metadata: string; + props: ChainProps; } export interface MetadataParams extends RootHashParams { - callData: string - signedExtensions: string + callData: string; + seIncludedInExtrinsic: string; + seIncludedInSignedData: string; +} +export interface MetadataParamsTxBlob extends RootHashParams { + txBlob: string; } -export declare function getShortMetadata(params: MetadataParams): string -export declare function getMetadataDigest(params: RootHashParams): string +export declare function getShortMetadataFromTxBlob(params: MetadataParamsTxBlob): string; +export declare function getShortMetadata(params: MetadataParams): string; +export declare function getMetadataDigest(params: RootHashParams): string; diff --git a/rust/src/helper.rs b/rust/src/helper.rs new file mode 100644 index 0000000..8be81af --- /dev/null +++ b/rust/src/helper.rs @@ -0,0 +1,77 @@ +use frame_metadata::RuntimeMetadata; +use merkleized_metadata::{FrameMetadataPrepared, SignedExtrinsicData,TypeResolver,CollectAccessedTypes}; +use scale_decode::visitor::decode_with_visitor; + +pub fn get_parts_len_from_tx_blob( + mut tx_blob: &[u8], + metadata: &RuntimeMetadata, +) -> Result, String> { + let mut call_data_len:usize = 0; + let mut signed_extensions_in_extrinsic_len:usize = 0; + + let prepared = FrameMetadataPrepared::prepare(metadata)?; + let type_information = prepared.as_type_information()?; + + let tx_blob_ptr = &mut tx_blob; + let tx_blob_ptr_init_len = tx_blob_ptr.len(); + + let type_resolver = TypeResolver::new(type_information.types.values()); + + let visitor = CollectAccessedTypes::default(); + + let mut visitor = decode_with_visitor( + tx_blob_ptr, + type_information.extrinsic_metadata.call_ty, + &type_resolver, + visitor, + ) + .map_err(|e| format!("Failed to decode call: {e}"))?; + + call_data_len = tx_blob_ptr_init_len - tx_blob_ptr.len(); + + if !tx_blob_ptr.is_empty() { + let included_in_extrinsic = &tx_blob_ptr.to_vec(); + let signed_ext_data:Option = Some(SignedExtrinsicData{ + included_in_extrinsic, + included_in_signed_data: &[], + }); + + let visitor = signed_ext_data + .map(|mut signed_ext_data| { + visitor.collect_all_types( + &type_information.extrinsic_metadata.address_ty, + &type_information, + ); + visitor.collect_all_types( + &type_information.extrinsic_metadata.signature_ty, + &type_information, + ); + + let included_in_extrinsic_ptr = &mut signed_ext_data.included_in_extrinsic; + let initial_len = included_in_extrinsic_ptr.len(); + + let fold_result = type_information + .extrinsic_metadata + .signed_extensions + .iter() + .try_fold(visitor.clone(), |visitor, se| { + decode_with_visitor( + included_in_extrinsic_ptr, + se.included_in_extrinsic, + &type_resolver, + visitor, + ) + .map_err(|e| format!("Failed to decode data in extrinsic ({}): {e}", se.identifier)) + }); + + // Handle the fold result and calculate the length + fold_result.map(|final_visitor| { + signed_extensions_in_extrinsic_len = initial_len - included_in_extrinsic_ptr.len(); + final_visitor + }) + }) + .unwrap_or_else(|| Ok(visitor))?; // Handle error case or provide default + } + + return Ok(Vec::from([call_data_len, signed_extensions_in_extrinsic_len])) +} \ No newline at end of file diff --git a/rust/src/lib.rs b/rust/src/lib.rs index e90901a..02c7327 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -1,137 +1,214 @@ +mod helper; + use frame_metadata::v15::RuntimeMetadataV15; use frame_metadata::{RuntimeMetadata, RuntimeMetadataPrefixed}; use merkleized_metadata::{ - generate_metadata_digest, generate_proof_for_extrinsic_parts, ExtraInfo, ExtrinsicMetadata, - FrameMetadataPrepared, Proof, + generate_metadata_digest, generate_proof_for_extrinsic_parts, ExtraInfo, ExtrinsicMetadata, + FrameMetadataPrepared, Proof, SignedExtrinsicData }; use neon::prelude::*; use parity_scale_codec::{Decode, Encode}; +use crate::helper::get_parts_len_from_tx_blob; #[derive(Encode)] pub struct MetadataProof { - proof: Proof, - extrinsic: ExtrinsicMetadata, - extra_info: ExtraInfo, + proof: Proof, + extrinsic: ExtrinsicMetadata, + extra_info: ExtraInfo, } fn get_extra_info<'a>( - cx: &mut impl Context<'a>, - js_props: Handle<'a, JsObject>, + cx: &mut impl Context<'a>, + js_props: Handle<'a, JsObject>, ) -> NeonResult { - let base58_prefix = js_props - .get_value(cx, "base58Prefix")? - .downcast::(cx) - .unwrap() - .value(cx) as u16; - let decimals = js_props - .get_value(cx, "decimals")? - .downcast::(cx) - .unwrap() - .value(cx) as u8; - let token_symbol = js_props - .get_value(cx, "tokenSymbol")? - .downcast::(cx) - .unwrap() - .value(cx); - let spec_name = js_props - .get_value(cx, "specName")? - .downcast::(cx) - .unwrap() - .value(cx); - let spec_version = js_props - .get_value(cx, "specVersion")? - .downcast::(cx) - .unwrap() - .value(cx) as u32; - Ok(ExtraInfo { - base58_prefix, - decimals, - token_symbol, - spec_name, - spec_version, - }) + let base58_prefix = js_props + .get_value(cx, "base58Prefix")? + .downcast::(cx) + .unwrap() + .value(cx) as u16; + let decimals = js_props + .get_value(cx, "decimals")? + .downcast::(cx) + .unwrap() + .value(cx) as u8; + let token_symbol = js_props + .get_value(cx, "tokenSymbol")? + .downcast::(cx) + .unwrap() + .value(cx); + let spec_name = js_props + .get_value(cx, "specName")? + .downcast::(cx) + .unwrap() + .value(cx); + let spec_version = js_props + .get_value(cx, "specVersion")? + .downcast::(cx) + .unwrap() + .value(cx) as u32; + Ok(ExtraInfo { + base58_prefix, + decimals, + token_symbol, + spec_name, + spec_version, + }) } fn get_short_metadata(mut cx: FunctionContext) -> JsResult { - let param_obj = cx.argument::(0).unwrap(); - let call_data_str = param_obj - .get_value(&mut cx, "callData")? - .downcast::(&mut cx) - .unwrap() - .value(&mut cx); - let sig_ext_str = param_obj - .get_value(&mut cx, "signedExtensions")? - .downcast::(&mut cx) - .unwrap() - .value(&mut cx); - let metadata_str = param_obj - .get_value(&mut cx, "metadata")? - .downcast::(&mut cx) - .unwrap() - .value(&mut cx); - let js_props = param_obj - .get_value(&mut cx, "props")? - .downcast::(&mut cx) - .unwrap(); - let specs = get_extra_info(&mut cx, js_props)?; - // The crate accepts now call data. We don't have to fake the signature info - let call_data = hex::decode(call_data_str).unwrap(); - let sig_ext = hex::decode(sig_ext_str).unwrap(); - let metadata = hex::decode(metadata_str).unwrap(); - - let runtime_meta_v15 = RuntimeMetadataV15::decode(&mut &metadata[5..]).unwrap(); - let runtime_meta = RuntimeMetadata::V15(runtime_meta_v15); - let registry_proof = - match generate_proof_for_extrinsic_parts(&call_data, Some(&sig_ext), &runtime_meta) { - Ok(x) => x, - Err(x) => return Ok(cx.string(x)), + let param_obj = cx.argument::(0).unwrap(); + let call_data_str = param_obj + .get_value(&mut cx, "callData")? + .downcast::(&mut cx) + .unwrap() + .value(&mut cx); + let se_included_in_extrinsic = param_obj + .get_value(&mut cx, "seIncludedInExtrinsic")? + .downcast::(&mut cx) + .unwrap() + .value(&mut cx); + let se_included_in_signed_data = param_obj + .get_value(&mut cx, "seIncludedInSignedData")? + .downcast::(&mut cx) + .unwrap() + .value(&mut cx); + let metadata_str = param_obj + .get_value(&mut cx, "metadata")? + .downcast::(&mut cx) + .unwrap() + .value(&mut cx); + let js_props = param_obj + .get_value(&mut cx, "props")? + .downcast::(&mut cx) + .unwrap(); + let specs = get_extra_info(&mut cx, js_props)?; + // The crate accepts now call data. We don't have to fake the signature info + let call_data = hex::decode(call_data_str).unwrap(); + let sig_ext = SignedExtrinsicData { + included_in_signed_data: &array_bytes::hex2bytes(se_included_in_signed_data).unwrap(), + included_in_extrinsic: &array_bytes::hex2bytes(se_included_in_extrinsic).unwrap(), }; - // Generates extrinsic_metadata in the same way the crate does - let extrinsic_metadata = FrameMetadataPrepared::prepare( - &RuntimeMetadataPrefixed::decode(&mut &metadata[..]) - .unwrap() - .1, - ) + let metadata = hex::decode(metadata_str).unwrap(); + let runtime_meta_v15 = RuntimeMetadataV15::decode(&mut &metadata[5..]).unwrap(); + let runtime_meta = RuntimeMetadata::V15(runtime_meta_v15); + + let registry_proof = + match generate_proof_for_extrinsic_parts(&call_data, Some(sig_ext), &runtime_meta) { + Ok(x) => x, + Err(x) => return Ok(cx.string(x)), + }; + + // Generates extrinsic_metadata in the same way the crate does + let extrinsic_metadata = FrameMetadataPrepared::prepare( + &RuntimeMetadataPrefixed::decode(&mut &metadata[..]) + .unwrap() + .1, + ) .unwrap() .as_type_information() .unwrap() .extrinsic_metadata; - let meta_proof = MetadataProof { - proof: registry_proof, - extrinsic: extrinsic_metadata, - extra_info: specs, - }; - Ok(cx.string(hex::encode(meta_proof.encode()))) + let meta_proof = MetadataProof { + proof: registry_proof, + extrinsic: extrinsic_metadata, + extra_info: specs, + }; + Ok(cx.string(hex::encode(meta_proof.encode()))) +} + +fn get_short_metadata_from_tx_blob(mut cx: FunctionContext) -> JsResult { + let param_obj = cx.argument::(0).unwrap(); + let tx_blob = param_obj + .get_value(&mut cx, "txBlob")? + .downcast::(&mut cx) + .unwrap() + .value(&mut cx); + let metadata_str = param_obj + .get_value(&mut cx, "metadata")? + .downcast::(&mut cx) + .unwrap() + .value(&mut cx); + let js_props = param_obj + .get_value(&mut cx, "props")? + .downcast::(&mut cx) + .unwrap(); + let specs = get_extra_info(&mut cx, js_props)?; + // The crate accepts now call data. We don't have to fake the signature info + let tx_blob = hex::decode(tx_blob).unwrap(); + + + let metadata = hex::decode(metadata_str).unwrap(); + let runtime_meta_v15 = RuntimeMetadataV15::decode(&mut &metadata[5..]).unwrap(); + let runtime_meta = RuntimeMetadata::V15(runtime_meta_v15); + + let parts_lens = + match get_parts_len_from_tx_blob(&tx_blob, &runtime_meta) { + Ok(x) => x, + Err(x) => return Ok(cx.string(x)), + }; + + let call_data = tx_blob[0..parts_lens[0]].to_vec(); + let se_included_in_extrinsic = tx_blob[parts_lens[0]..parts_lens[0] + parts_lens[1]].to_vec(); + let se_included_in_signed_data = tx_blob[parts_lens[0] + parts_lens[1]..].to_vec(); + + let sig_ext = SignedExtrinsicData { + included_in_signed_data: &se_included_in_signed_data, + included_in_extrinsic: &se_included_in_extrinsic, + }; + let registry_proof = + match generate_proof_for_extrinsic_parts(&call_data, Some(sig_ext), &runtime_meta) { + Ok(x) => x, + Err(x) => return Ok(cx.string(x)), + }; + + // Generates extrinsic_metadata in the same way the crate does + let extrinsic_metadata = FrameMetadataPrepared::prepare( + &RuntimeMetadataPrefixed::decode(&mut &metadata[..]) + .unwrap() + .1, + ) + .unwrap() + .as_type_information() + .unwrap() + .extrinsic_metadata; + + let meta_proof = MetadataProof { + proof: registry_proof, + extrinsic: extrinsic_metadata, + extra_info: specs, + }; + Ok(cx.string(hex::encode(meta_proof.encode()))) } fn get_metadata_digest(mut cx: FunctionContext) -> JsResult { - let param_obj = cx.argument::(0).unwrap(); - let metadata_str = param_obj - .get_value(&mut cx, "metadata")? - .downcast::(&mut cx) - .unwrap() - .value(&mut cx); - let js_props = param_obj - .get_value(&mut cx, "props")? - .downcast::(&mut cx) - .unwrap(); - let extra_info = get_extra_info(&mut cx, js_props)?; - let metadata = hex::decode(metadata_str).unwrap(); - let runtime_meta_v15 = RuntimeMetadataV15::decode(&mut &metadata[5..]).unwrap(); - let runtime_meta = RuntimeMetadata::V15(runtime_meta_v15); - - let digest = match generate_metadata_digest(&runtime_meta, extra_info) { - Ok(x) => hex::encode(x.hash()), - Err(x) => x, - }; - Ok(cx.string(digest)) + let param_obj = cx.argument::(0).unwrap(); + let metadata_str = param_obj + .get_value(&mut cx, "metadata")? + .downcast::(&mut cx) + .unwrap() + .value(&mut cx); + let js_props = param_obj + .get_value(&mut cx, "props")? + .downcast::(&mut cx) + .unwrap(); + let extra_info = get_extra_info(&mut cx, js_props)?; + let metadata = hex::decode(metadata_str).unwrap(); + let runtime_meta_v15 = RuntimeMetadataV15::decode(&mut &metadata[5..]).unwrap(); + let runtime_meta = RuntimeMetadata::V15(runtime_meta_v15); + + let digest = match generate_metadata_digest(&runtime_meta, extra_info) { + Ok(x) => hex::encode(x.hash()), + Err(x) => x, + }; + Ok(cx.string(digest)) } #[neon::main] fn main(mut cx: ModuleContext) -> NeonResult<()> { - cx.export_function("getShortMetadata", get_short_metadata)?; - cx.export_function("getMetadataDigest", get_metadata_digest)?; - Ok(()) + cx.export_function("getShortMetadata", get_short_metadata)?; + cx.export_function("getShortMetadataFromTxBlob", get_short_metadata_from_tx_blob)?; + cx.export_function("getMetadataDigest", get_metadata_digest)?; + Ok(()) } diff --git a/src/server.ts b/src/server.ts index 301312f..467aee9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,7 +3,7 @@ import bodyParser from 'body-parser' import http from 'http' import { cacheMetadata } from './utils/metadata' -import { getShortMetadata } from '../rust' +import { getShortMetadataFromTxBlob } from '../rust' import { Chain, loadChains } from './utils/chains' interface ChainConfig { @@ -11,8 +11,7 @@ interface ChainConfig { } interface TxToSign { - callData: string - signedExtensions: string + txBlob: string chain: ChainConfig } @@ -79,7 +78,7 @@ export function createAndServe() { const { chain: { id: chainId }, }: TxToSign = req.body - let { callData, signedExtensions }: TxToSign = req.body + let { txBlob }: TxToSign = req.body const chain = chains.find((b: Chain) => b.id === chainId) if (!chain) { @@ -102,24 +101,17 @@ export function createAndServe() { return } - if (!callData) { - res.status(400).send('callData is missing') - return - } - if (!signedExtensions) { - res.status(400).send('signedExtensions is missing') + if (!txBlob) { + res.status(400).send('txBlob is missing') return } try { - if (callData.substring(0, 2) == '0x') { - callData = callData.substring(2) - } - if (signedExtensions.substring(0, 2) == '0x') { - signedExtensions = signedExtensions.substring(2) + if (txBlob.substring(0, 2) == '0x') { + txBlob = txBlob.substring(2) } - const txMetadata = Buffer.from(getShortMetadata({ callData, signedExtensions, metadata: metadataHex, props }), 'hex') + const txMetadata = Buffer.from(getShortMetadataFromTxBlob({ txBlob, metadata: metadataHex, props }), 'hex') res.status(200).send({ txMetadata: '0x' + txMetadata.toString('hex') }) } catch (e) { res.status(500).send(e) diff --git a/tests/common.test.ts b/tests/common.test.ts index f7906d8..7b9e507 100644 --- a/tests/common.test.ts +++ b/tests/common.test.ts @@ -32,14 +32,15 @@ describe('basic api', () => { test('get tx metadata', async () => { const resp = await axios.post('http://127.0.0.1:3001/transaction/metadata', { - callData: '0000d050f0c8c0a9706b7c0c4e439245a347627901c89d4791239533d1d2c961f1a72ad615c8530de078e565ba644b38b01bcad249e8c0', - signedExtensions: - 'a80aceb4befe330990a59f74ed976c933db269c64dda40104a0f001900000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3a071db11cdbfd29285f25d402f1aee7a1c0384269c9c2edb476688d35e346998', + txBlob: + '0000d050f0c8c0a9706b7c0c4e439245a347627901c89d4791239533d1d2c961f1a72ad615c8530de078e565ba644b38b01bcad249e8c0a80aceb4befe330990a59f74ed976c933db269c64dda40104a0f001900000091b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3a071db11cdbfd29285f25d402f1aee7a1c0384269c9c2edb476688d35e346998', chain: { id: 'dot' }, }) expect(resp.status).toBe(200) - expect(resp.data).not.toBe(undefined) + expect(resp.data.txMetadata).toBe( + '0x60000341000000030102082873705f72756e74696d65384d756c74695369676e6174757265011c456432353531390400169d010148656432353531393a3a5369676e617475726500c902082873705f72756e74696d65384d756c74695369676e6174757265011c53723235353139040016cd020148737232353531393a3a5369676e617475726504c902082873705f72756e74696d65384d756c74695369676e617475726501144563647361040016d102014065636473613a3a5369676e617475726508c9020c1c73705f636f72651c73723235353139245369676e617475726500040016a10101205b75383b2036345dcd020c1c73705f636f7265146563647361245369676e6174757265000400160102017c5b75383b205349474e41545552455f53455249414c495a45445f53495a455dd10210306672616d655f73797374656d28657874656e73696f6e733c636865636b5f6d6f7274616c69747938436865636b4d6f7274616c697479000400168506010c4572618106102873705f72756e74696d651c67656e657269630c6572610c45726101244d6f7274616c31363804000300a102850610306672616d655f73797374656d28657874656e73696f6e732c636865636b5f6e6f6e636528436865636b4e6f6e6365000400110120543a3a4e6f6e63658906086870616c6c65745f7472616e73616374696f6e5f7061796d656e74604368617267655472616e73616374696f6e5061796d656e7400040013013042616c616e63654f663c543e8d060c1c73705f636f72651863727970746f2c4163636f756e7449643332000400160401205b75383b2033325d000003200000000304083c7072696d69746976655f74797065731048323536000400160401205b75383b2033325d0c00020310000314000000035c0840706f6c6b61646f745f72756e74696d652c52756e74696d6543616c6c011853797374656d040016cc01ad0173656c663a3a73705f6170695f68696464656e5f696e636c756465735f636f6e7374727563745f72756e74696d653a3a68696464656e5f696e636c7564653a3a64697370617463680a3a3a43616c6c61626c6543616c6c466f723c53797374656d2c2052756e74696d653e00c80c306672616d655f73797374656d1870616c6c65741043616c6c011872656d61726b04011872656d61726b1610011c5665633c75383e00cc0c2873705f72756e74696d65306d756c746961646472657373304d756c746941646472657373010849640400160001244163636f756e7449640019010c2873705f72756e74696d65306d756c746961646472657373304d756c7469416464726573730114496e64657804001501304163636f756e74496e6465780419010c2873705f72756e74696d65306d756c746961646472657373304d756c746941646472657373010c52617704001610011c5665633c75383e0819010c2873705f72756e74696d65306d756c746961646472657373304d756c74694164647265737301244164647265737333320400160401205b75383b2033325d0c19010c2873705f72756e74696d65306d756c746961646472657373304d756c74694164647265737301244164647265737332300400165c01205b75383b2032305d1019010c1c73705f636f72651c65643235353139245369676e617475726500040016a10101205b75383b2036345d9d0100034000000003a10160140800009508000096080000970800009808000099080000c80b0000710c0000c90c0000ca0c000065060000660600006806000069060000a10600000a070000360700006507000066070000670700006807000069070000be070000bf07000025016789ced43a0367885ff3078f5e2fc7a4f410e0de25fc61592fd571a3ddd0992308c0aeb6a568a4d74f481cbb138a78dfb713c343c20a1050c5235b1ad89073b2cb142cc9a3b3bdfcd9ed0e511c36917ae671cc251a6d7fd3d5f85d9a5e7c3fa5c3f90b255b14f7de7904a8a82d6f793db29ea554a3a72d5d7cc8ce6d7737bc0173743df5f2da9aabcea1dd2dbd238bddc043e8ea38535ab63a7992bc96849747568b99743425c3b7952dce707b4f9a80fa40118ef72aaf9dc3f5395ae644511d49fed1b2ae921ef2b57e8ef1f97884c95cb192eb72aff9121b362d353c7607b20d63fcfc9db22e37fbd115d20c0a95bcff4e1a2d198c2c46f56cddbb36c1e3337096be7ded3a487b812f391541f80b87b4a6e5b02e6547f6e87f41a60199a4930eaf1a762ca945720736ba47951700d451e5a5632995a575be272e8299e1210f02304678945b1d3661789ee3b3c94b09cb8210d6997b610086b2f768f914314cbd4b0da441e1d200352230239b6bb43a187e22d1ee788737c0fb38e06a84ae1d7465e3499cb65f80604723105a3c752946df8f689de24809d9a32e785697c476acf0de41e9616da3b42a902176dd8708edd4fded453de1482673dd739e1126551d7657b1d4c973a62afb0fb87ca4023059f8778235367e3664ffb34c6d651737c1c29bf8b17a01b7c6524a998d958d03052f7294aaceaa07c0ce2ccc35cd08872f436caf9d39af986a70fe2923b8a433472c431b18b01a3801d02f187102464cda4c9376ec604df012735ff23739dfc232e0daade3f6e438786f035cf6c8a343efefbf92dfbdf739ca2cb82d84a288bace231a23497fb059ba78dde5d7b41ea49145f8878601aca7c0aed379d9dab0fdf35d4c262dff0664845b741fff26953a5d2c7a489d907b186551a787f47de0fd1d606dfdb6d0c31b568b1e6d21caec884c91aa88564aff5e9c8ee47b3753091ee7fe59d3d68b279dee5d8c28cdd86428a5f1789a440bf1f887e14bc68dca767b0b76b9e253c7f7e4abaf9fcfa4dbcc7a6f537dbed88cf50a7d9686710ddb0144d59907efacb765d05c3456f7061587dce2c7986c2d6dc666d786b91d4a191a677f4b402f6c7e851f4116ad33fef6c4a21b4429b2a4b1c47797e563e87aad368ed6861216f2f4759185f52d75af2a31a2a8f2b10a5087502003f1890da1e82538c9a4065127b9049b877a7ac6638a3b559c1267d7b4f49eeb6d62f952bf666e72d11a0422c3c91c092434c1fd314d9ec50fbed16620ff0bb4542c5a86be835d03e0cfd6734d58523c5a756263416230e1b37cc13bce2426531ca24fbdd8bf0dbe3c0f85d7bd98c1fb6df0c78fc2e9f2702f34140a8c7c613af848034f46ac6459942b40ab97d3a440d3225846cc1f69cf051d8b894b8d44de727dbeb2bab466f3f1a0e7f9e8db72e7014adc6407d1e6cf27a078a0ecf35cc230f109b9b6febea4f045685937351667b4f9c5d203ac57a2b6192da66752008cbe46e4b9a4d1adcd5c738939da73dd4e98c81244e77c304fd3814e151905ab6499c76ccb290d3cc9fa2f41bd00139140b7206e31ff9d98e4beef1d1a2e20bbc4a225773b9327590503729e589420b1a75d86eaabff22d7ceb0a3bce7985ac4ce86e36c57c1716dc9c7e103b2ad5a424a8d0e86dfa17f9a0777cea925237e5e01f242991ef6c33a086b7c0e4bc8c497b88128da54855c26a44774166c1c0c12df62186723dc72187a726913882137fa3921e1778031871fbc47ddf9eaa50b0a79749430145a6491ea04b932fad174e2c3ba25099cdf2ff6fb93e59312a2391582fa6301d039ad6cb34be631295a583d696acdf81f60aef2933d103a4f6838dc8a34267a4bc34f96ba701316325e7166e9d994eaafae1a36114b99fc77cd003b826a4eb3284fb97de0abe331c71e30d7222190ed1ceb320a5e235f247ae4a367e39345f284d7d157b7dab0b11918ccc7d855c89675584a017fae32fbe8f6e6a00d4f2214646fb95b88b2be76a5f3183f890b6757cf45d3847c454a1bf766c077626a49c05f295b5df428a3792a70b1150f81b8f2dfbd1614c71be9c51a53a8cc366b5da97f26546986169bac68e066dc4921c50d3d31f7b93167d1ff3f42b1237a45ffbf39116c9291fa5754e677cdb0b6c1c29094f048d777bd704b36ab9f431fdee3f8440ad530699b9b81bece174a4c0e6c8a849749b3230acc366d2f1faf5663f98fbe2bde7db2293400e29779d28c921ff4517841e98812fcd13389cab1b67d198b1b829616cc39a2f77f50ee706247fa049e8d3a3e25d3c203a6bdafa9c852f00cb804c9346f09c5f75932f0260d7d6b64623771df07d7d793501f5d06be845c97b32a7a75878614cfd3750a3f39f792a36e025437cb75c8e12c606802f40a9540865368acbde951ec8d2b89f04d2a6d2168ff4d215b0125b253e95a7cb55e5ade1e6ac5da9d5d58cb5e2cd5d18d25ae3fb918f214abbdc69c5122550e019b1451f642b7c1727544a7875eca6bf30116c5f75c826f20c8df8e0e1bc4ba6c84115749a32cd464b7596912e5aed4e3acc77016ade848daafb938d5f7f4744d7adfdccc862df5c8ca239a35a773543764f1d1a26072c68f516029097636910f24e375120482d2fc6daf35102f7c29bd9813d030d156bd0face37a47937be063b0266760766cd46d835764bdff25e24df5ed38f32e0d8da21c1c8de4c9ee8a0b7a4f70c08c9efc0ace26aef368b8aa5c34bef19a95ec79f4a17a53cb7b46b03b872e2eeb8d11b608248a4bf93910ca8dcbb9ddbb45f6b6f137da120bdd23b2835495bdb293da5f7195f338b604036fc9934315b19b8879243319d62ec6683a4929a57b63a0e8448039385ad11e6e71250c61bc4e8f0958fc30aa2702eae5209084ab6c5b03c3e178acb187ce8cafacd47717fc1c95e36d0451a59ac29716afcb1c9b60ae84e1cddf6eb72179718426c7e76996a32f8d3a1c3571406e43c3c92e66a85453bafc09db87a5823738aee33d23d358145b3538e76d4659e74cd5f5548871e6dd103ba4af5d6ff6f170de073361d9ea751683cba3837e2820de029ed100c52ed286700051b1dc8d94fc887a7e3bf71fe5cae00dc09e26c5beaf0bb8dc2785524253ac88b3dd7a38a6b53c4b86878140c4747eb4b3e24ce100674c456d5a40c2471bd6f9f539778f78129b9790936115677923895ffce590b5b70cd637ae1c497718a5cd5178022f4ae71d85b12d9fcf1766754b3548fef3621b3fb9b21b11e9f902f8cd4e4ef20f0556d5ac48834de84ba0a9cea50416190116c816c9022448436865636b4e6f6e5a65726f53656e646572151540436865636b5370656356657273696f6e150538436865636b547856657273696f6e150530436865636b47656e6573697315160c38436865636b4d6f7274616c697479168106160c28436865636b4e6f6e6365168906152c436865636b5765696768741515604368617267655472616e73616374696f6e5061796d656e74168d06154850726576616c6964617465417474657374731515104a0f0020706f6c6b61646f7400000a0c444f54', + ) }) test('flush chain metadata', async () => {