From 17946fb0f4b29d83f0c6ea85ae3217a8f1e1980d Mon Sep 17 00:00:00 2001 From: clearloop Date: Tue, 15 Sep 2020 09:20:18 +0800 Subject: [PATCH] Update the receipt API (#81) * chore(api): update the types of receipt api * refactor(listener): separated relay listener into files * feat(proxy): supports proxy in the relay process * feat(bin): add binary dj-proposal for testing proposal directly * chore(api): update the confirmed block api * feat(cmd): add command `confirm` * fix(redeem): correct the redeem loop * chore(bin): add binary tag to new bins --- package.json | 12 +-- src/api/darwinia.ts | 47 ++++++++--- src/api/shadow.ts | 6 +- src/bin/confirm.ts | 34 ++++++++ src/bin/proposal.ts | 34 ++++++++ src/listener/guard.ts | 9 +-- src/listener/index.ts | 4 +- src/listener/relay.ts | 139 -------------------------------- src/listener/relay/approve.ts | 21 +++++ src/listener/relay/index.ts | 42 ++++++++++ src/listener/relay/new_round.ts | 54 +++++++++++++ src/listener/relay/relay.ts | 32 ++++++++ src/types.ts | 10 ++- src/util/cfg.ts | 3 + src/util/static/config.json | 47 +++++------ 15 files changed, 299 insertions(+), 195 deletions(-) create mode 100644 src/bin/confirm.ts create mode 100644 src/bin/proposal.ts delete mode 100644 src/listener/relay.ts create mode 100644 src/listener/relay/approve.ts create mode 100644 src/listener/relay/index.ts create mode 100644 src/listener/relay/new_round.ts create mode 100644 src/listener/relay/relay.ts diff --git a/package.json b/package.json index 2a11503..2059049 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": false, "name": "@darwinia/dj", - "version": "", + "version": "0.2.3-beta.2", "description": "Darwinia bridge relayer tool", "homepage": "https://github.com/darwinia-network/dj", "repository": { @@ -12,12 +12,14 @@ "license": "GPL-3.0", "main": "lib/index.js", "bin": { - "dj": "lib/index.js" + "dj": "lib/index.js", + "dj-proposal": "lib/src/bin/proposal.js", + "dj-confirm": "lib/src/bin/confirm.js" }, "files": ["lib/**/*"], "dependencies": { - "@polkadot/api": "1.30.0-beta.0", - "@polkadot/keyring": "3.3.1", + "@polkadot/api": "1.32.1", + "@polkadot/keyring": "3.4.1", "@polkadot/util-crypto": "3.3.1", "axios": "^0.19.2", "prompts": "^2.3.2", @@ -27,7 +29,7 @@ "devDependencies": { "@commitlint/cli": "^8.3.5", "@commitlint/config-conventional": "^8.3.4", - "@polkadot/types": "1.30.0-beta.0", + "@polkadot/types": "1.32.1", "@types/node": "^13.11.1", "@types/prompts": "^2.0.8", "husky": "^4.2.5", diff --git a/src/api/darwinia.ts b/src/api/darwinia.ts index 2513f4b..a32d85b 100644 --- a/src/api/darwinia.ts +++ b/src/api/darwinia.ts @@ -2,11 +2,10 @@ import { log, Config } from "../util"; import { ApiPromise, SubmittableResult, WsProvider } from "@polkadot/api"; import { SubmittableExtrinsic } from "@polkadot/api/types"; -import Keyring from "@polkadot/keyring"; +import { Keyring, decodeAddress } from "@polkadot/keyring"; import { KeyringPair } from "@polkadot/keyring/types"; import { DispatchError, EventRecord } from "@polkadot/types/interfaces/types"; import { cryptoWaitReady } from "@polkadot/util-crypto"; -import { SignedBlock } from "@polkadot/types/interfaces"; import { IEthereumHeaderThingWithProof, IReceiptWithProof, @@ -83,7 +82,7 @@ export class API { public static async auto(): Promise { const cfg = new Config(); const seed = await cfg.checkSeed(); - return await API.new(seed, cfg.node, cfg.types); + return await API.new(seed, cfg.node, cfg.relayer, cfg.types); } /** @@ -123,6 +122,7 @@ export class API { public static async new( seed: string, node: string, + relayer: string, types: Record, ): Promise { const api = await ApiPromise.create({ @@ -132,10 +132,12 @@ export class API { const account = await API.seed(seed); log.trace("init darwinia api succeed"); - return new API(account, (api as ApiPromise), types); + const relayerAddr = relayer.length > 0 ? decodeAddress(relayer) : new Uint8Array(); + return new API(account, (api as ApiPromise), relayerAddr, types); } public account: KeyringPair; + public relayer: Uint8Array; public types: Record; public _: ApiPromise; @@ -147,8 +149,14 @@ export class API { * @param {KeyringPair} account - darwinia account * @param {ApiPromise} ap - raw polkadot api */ - constructor(account: KeyringPair, ap: ApiPromise, types: Record) { + constructor( + account: KeyringPair, + ap: ApiPromise, + relayer: Uint8Array, + types: Record, + ) { this.account = account; + this.relayer = relayer; this.types = types; this._ = ap; } @@ -157,12 +165,13 @@ export class API { * Get last confirm block */ public async lastConfirm(): Promise { - const res = await this._.query.ethereumRelay.lastConfirmedHeaderInfo(); + const res = await this._.query.ethereumRelay.confirmedBlockNumbers(); if (res.toJSON() === null) { return 0; } - return (res.toJSON() as any)[0] as number; + const blocks = res.toJSON() as number[]; + return blocks[blocks.length - 1]; } /** @@ -207,6 +216,15 @@ export class API { return await this.blockFinalized(ex); } + /** + * Set confirmed block with sudo privilege + */ + public async setConfirmed(headerThing: IEthereumHeaderThingWithProof): Promise { + log.event(`Set confirmed block ${headerThing.header.number}`); + const ex = this._.tx.ethereumRelay.setConfirmed(headerThing); + return await this.blockFinalized(this._.tx.sudo.sudo(ex)); + } + /** * get the specify block * @@ -214,16 +232,19 @@ export class API { */ public async submitProposal(headerThings: IEthereumHeaderThingWithProof[]): Promise { const latest = headerThings[headerThings.length - 1].header.number; - const cts = ((await this._.query.ethereumRelay.confirmedHeadersDoubleMap( - Math.floor(latest / 185142), latest, - )).toJSON() as any).timestamp; - if (cts !== 0) { + const confirmed = await this._.query.ethereumRelay.confirmedHeaders(latest); + if (confirmed.toJSON()) { + log.event(`Proposal ${latest} has been submitted yet`); return new ExResult(true, "", ""); } // Submit new proposal - log.event(`Submit proposal contains block ${headerThings[headerThings.length - 1].header.number}`); - const ex = this._.tx.ethereumRelay.submitProposal(headerThings); + log.event(`Submit proposal contains block ${latest}`); + let ex = this._.tx.ethereumRelay.submitProposal(headerThings); + if (this.relayer.length > 0) { + ex = this._.tx.proxy.proxy(this.relayer, "EthereumBridge", ex); + } + return await this.blockFinalized(ex); } diff --git a/src/api/shadow.ts b/src/api/shadow.ts index 05e7667..d3f8113 100644 --- a/src/api/shadow.ts +++ b/src/api/shadow.ts @@ -7,7 +7,6 @@ import { IEthereumHeaderThingWithProof, } from "../types"; - /** * Shadow APIs * @@ -20,7 +19,6 @@ export class ShadowAPI { axios.defaults.proxy = false; } - /** * Get darwinia block with eth proof * @@ -59,13 +57,13 @@ export class ShadowAPI { * @param {number} block - block number */ async getProposal( - leaves: number[], + member: number, target: number, last_leaf: number, ): Promise { log.event(`Fetching proposal of ${target}`); const r: any = await axios.post("/eth/proposal", { - leaves, + member, target, last_leaf, }).catch(log.err); diff --git a/src/bin/confirm.ts b/src/bin/confirm.ts new file mode 100644 index 0000000..ee7ca25 --- /dev/null +++ b/src/bin/confirm.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import { Config, log } from "../util"; +import { API, ShadowAPI } from "../api"; + +(async () => { + const args = process.argv.slice(2); + if (args.length !== 1) { + log.warn("Usage: dj-confirm "); + return; + } + + /// Init logs + process.env.LOGGER = "ALL"; + + /// Init API + const conf = new Config(); + const api = await API.auto(); + const shadow = new ShadowAPI(conf.shadow); + const target = Number.parseInt(args[0], 10); + + // Trigger relay + const lastConfirmed = await api.lastConfirm(); + try { + await api.setConfirmed(await shadow.getProposal( + lastConfirmed, + target, + target - 1, + )); + + log.ox(`Set confirmed block ${target} succeed!`); + } catch (e) { + log.ex(e); + } +})(); diff --git a/src/bin/proposal.ts b/src/bin/proposal.ts new file mode 100644 index 0000000..e314448 --- /dev/null +++ b/src/bin/proposal.ts @@ -0,0 +1,34 @@ +#!/usr/bin/env node +import { Config, log } from "../util"; +import { API, ShadowAPI } from "../api"; + +(async () => { + const args = process.argv.slice(2); + if (args.length !== 1) { + log.warn("Usage: dj-proposal "); + return; + } + + /// Init logs + process.env.LOGGER = "ALL"; + + /// Init API + const conf = new Config(); + const api = await API.auto(); + const shadow = new ShadowAPI(conf.shadow); + const target = Number.parseInt(args[0], 10); + + // Trigger relay + const lastConfirmed = await api.lastConfirm(); + try { + await api.submitProposal([await shadow.getProposal( + lastConfirmed, + target, + target - 1, + )]); + + log.ox(`Submitted proposal ${target}`); + } catch (e) { + log.ex(e); + } +})(); diff --git a/src/listener/guard.ts b/src/listener/guard.ts index c8594ae..b4438bd 100644 --- a/src/listener/guard.ts +++ b/src/listener/guard.ts @@ -2,7 +2,7 @@ import { ShadowAPI, API, ExResult } from "../api"; import { log } from "../util"; // Proposal guard -export async function guard(api: API, shadow: ShadowAPI) { +export async function listen(api: API, shadow: ShadowAPI) { let perms = 4; if ((await api._.query.sudo.key()).toJSON().indexOf(api.account.address) > -1) { perms = 7; @@ -13,20 +13,17 @@ export async function guard(api: API, shadow: ShadowAPI) { } // start listening - let lock = false; const handled: number[] = []; setInterval(async () => { - if (lock) { return; } const headers = (await api._.query.ethereumRelayerGame.pendingHeaders()).toJSON() as string[][]; if (headers.length === 0) { return; } - lock = true; for (const h of headers) { const blockNumber = Number.parseInt(h[1], 10); if (handled.indexOf(blockNumber) > -1) { - break; + continue; } const block = (await shadow.getHeaderThing(blockNumber)) as any; @@ -47,7 +44,5 @@ export async function guard(api: API, shadow: ShadowAPI) { } handled.push(blockNumber); } - - lock = false; }, 10000); } diff --git a/src/listener/index.ts b/src/listener/index.ts index 3c56c7d..d2d82ad 100644 --- a/src/listener/index.ts +++ b/src/listener/index.ts @@ -1,4 +1,4 @@ -export { guard } from "./guard"; export * as Cache from "./cache"; -export { relay } from "./relay"; +export { listen as guard } from "./guard"; +export { listen as relay } from "./relay"; export { listen as ethereum } from "./eth"; diff --git a/src/listener/relay.ts b/src/listener/relay.ts deleted file mode 100644 index b945640..0000000 --- a/src/listener/relay.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { ShadowAPI, API } from "../api"; -import { log, delay } from "../util"; -import { DispatchError } from "@polkadot/types/interfaces/types"; -import { IEthereumHeaderThingWithProof, ITx } from "../types"; -import { Cache } from "./" - -// Listen and submit proposals -export function relay(api: API, shadow: ShadowAPI, queue: ITx[]) { - const submitted: number[] = []; - - // Trigger relay every 180s - setInterval(async () => { - if (queue.length < 1) return; - - // Check last confirm - const lastConfirmed = await api.lastConfirm(); - let target = Math.max( - lastConfirmed + 1, - queue.sort((p, q) => q.blockNumber - p.blockNumber)[0].blockNumber, - ); - - // Check target - while (submitted.indexOf(target) > -1) { - target += 1; - } - - // Submit new proposal - log(`Currently we have ${queue.length} txs are waiting to be redeemed`); - await api.submitProposal([await shadow.getProposal( - [lastConfirmed], - target, - target - 1, - )]).catch(log.err); - - // Refresh target - submitted.push(target); - }, 180000); - - // Subscribe to system events via storage - api._.query.system.events((events: any) => { - events.forEach(async (record: any) => { - const { event, phase } = record; - // const types = event.typeDef; - - switch (event.method) { - case "GameOver": - gameOver(); - case "PendingHeaderApproved": - approved(event, phase, api, shadow, queue); - case "NewRound": - // TODO - // - // Fix the Relayer Game API - // await newRound(event, phase, types, api, shadow); - } - - if (event.data[0] && (event.data[0] as DispatchError).isModule) { - log.err(api._.registry.findMetaError( - (event.data[0] as DispatchError).asModule.toU8a(), - )); - } - }); - }); -} - -/// GameOver handler -function gameOver() { - log.trace("Gameover"); -} - -/// Approved handler -async function approved( - event: any, - phase: any, - api: API, - shadow: ShadowAPI, - queue: ITx[], -) { - const lastConfirmed = await api.lastConfirm(); - log.trace(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`); - log.trace(`\t\t${event.meta.documentation.toString()}`); - for (const tx of queue.filter((ftx) => ftx.blockNumber < lastConfirmed)) { - const curLastConfirmed = await api.lastConfirm(); - await api.redeem(tx.ty, await shadow.getReceipt(tx.tx, curLastConfirmed)); - await delay(5000); - }; - - queue = queue.filter((tx) => tx.blockNumber >= lastConfirmed); -} - -/// NewRound handler -async function _newRound( - event: any, - phase: any, - types: any, - api: API, - shadow: ShadowAPI, -) { - log.trace(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`); - log.trace(`\t\t${event.meta.documentation.toString()}`); - log.trace(JSON.stringify(event.data.toJSON())); - - // Samples - const lastLeaf = Math.max(...(event.data[0].toJSON() as number[])); - let members: number[] | number = event.data[0].toJSON() as number[]; - if (members === undefined) { - return - } else if (!Array.isArray(members)) { - members = [members as number]; - } - - // Get proposals - let newMember: number = 0; - let proposals: IEthereumHeaderThingWithProof[] = []; - (members as number[]).forEach((i: number) => { - const block = Cache.getBlock(i); - if (block) { - proposals.push(block); - } else { - newMember = i; - } - }) - - const newProposal = await shadow.getProposal([newMember], newMember, lastLeaf); - Cache.setBlock(newMember, Object.assign(JSON.parse(JSON.stringify(newProposal)), { - ethash_proof: [], - mmr_root: "", - mmr_proof: [], - })); - proposals = proposals.concat(newProposal); - - // Submit new proposals - await api.submitProposal(proposals); - - // Loop through each of the parameters, displaying the type and data - event.data.forEach((data: any, index: any) => { - log.trace(`\t\t\t${types[index].type}: ${data.toString()}`); - }); -} diff --git a/src/listener/relay/approve.ts b/src/listener/relay/approve.ts new file mode 100644 index 0000000..9127985 --- /dev/null +++ b/src/listener/relay/approve.ts @@ -0,0 +1,21 @@ +import { ShadowAPI, API } from "../../api"; +import { log, delay } from "../../util"; +import { ITx } from "../../types"; + +/// Approved handler +export default async function approved( + event: any, + phase: any, + api: API, + shadow: ShadowAPI, + queue: ITx[], +) { + log.trace(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`); + log.trace(`\t\t${event.meta.documentation.toString()}`); + + for (const tx of queue) { + const curLastConfirmed = await api.lastConfirm(); + await api.redeem(tx.ty, await shadow.getReceipt(tx.tx, curLastConfirmed)); + await delay(10000); + }; +} diff --git a/src/listener/relay/index.ts b/src/listener/relay/index.ts new file mode 100644 index 0000000..c50d9b4 --- /dev/null +++ b/src/listener/relay/index.ts @@ -0,0 +1,42 @@ +import { ShadowAPI, API } from "../../api"; +import { log } from "../../util"; +import { DispatchError } from "@polkadot/types/interfaces/types"; +import { ITx } from "../../types"; + +import approve from "./approve"; +import relay from "./relay"; + +export function listen(api: API, shadow: ShadowAPI, queue: ITx[]) { + relay(api, shadow, queue); + + // Subscribe to system events via storage + api._.query.system.events((events: any) => { + events.forEach(async (record: any) => { + const { event, phase } = record; + // const types = event.typeDef; + + switch (event.method) { + case "GameOver": + log.event("GameOver"); + case "PendingHeaderApproved": + const lastConfirmed = await api.lastConfirm(); + await approve( + event, phase, api, shadow, + queue.filter((ftx) => ftx.blockNumber < lastConfirmed), + ); + queue = queue.filter((ftx) => ftx.blockNumber >= lastConfirmed); + case "NewRound": + // TODO + // + // Fix the Relayer Game API + // await newRound(event, phase, types, api, shadow); + } + + if (event.data[0] && (event.data[0] as DispatchError).isModule) { + log.err(api._.registry.findMetaError( + (event.data[0] as DispatchError).asModule.toU8a(), + )); + } + }); + }); +} diff --git a/src/listener/relay/new_round.ts b/src/listener/relay/new_round.ts new file mode 100644 index 0000000..5ca4d67 --- /dev/null +++ b/src/listener/relay/new_round.ts @@ -0,0 +1,54 @@ +import { ShadowAPI, API } from "../../api"; +import { log } from "../../util"; +import { IEthereumHeaderThingWithProof } from "../../types"; +import { Cache } from "../" + +/// NewRound handler +export default async function newRound( + event: any, + phase: any, + types: any, + api: API, + shadow: ShadowAPI, +) { + log.trace(`\t${event.section}:${event.method}:: (phase=${phase.toString()})`); + log.trace(`\t\t${event.meta.documentation.toString()}`); + log.trace(JSON.stringify(event.data.toJSON())); + + // Samples + const lastLeaf = Math.max(...(event.data[0].toJSON() as number[])); + let members: number[] | number = event.data[0].toJSON() as number[]; + if (members === undefined) { + return + } else if (!Array.isArray(members)) { + members = [members as number]; + } + + // Get proposals + let newMember: number = 0; + let proposals: IEthereumHeaderThingWithProof[] = []; + (members as number[]).forEach((i: number) => { + const block = Cache.getBlock(i); + if (block) { + proposals.push(block); + } else { + newMember = i; + } + }) + + const newProposal = await shadow.getProposal(newMember, newMember, lastLeaf); + Cache.setBlock(newMember, Object.assign(JSON.parse(JSON.stringify(newProposal)), { + ethash_proof: [], + mmr_root: "", + mmr_proof: [], + })); + proposals = proposals.concat(newProposal); + + // Submit new proposals + await api.submitProposal(proposals); + + // Loop through each of the parameters, displaying the type and data + event.data.forEach((data: any, index: any) => { + log.trace(`\t\t\t${types[index].type}: ${data.toString()}`); + }); +} diff --git a/src/listener/relay/relay.ts b/src/listener/relay/relay.ts new file mode 100644 index 0000000..4a30de0 --- /dev/null +++ b/src/listener/relay/relay.ts @@ -0,0 +1,32 @@ +import { ShadowAPI, API } from "../../api"; +import { log } from "../../util"; +import { ITx } from "../../types"; + +// Listen and submit proposals +export default function relay(api: API, shadow: ShadowAPI, queue: ITx[]) { + const submitted: number[] = []; + + // Trigger relay every 180s + setInterval(async () => { + if (queue.length < 1) return; + + // Check last confirm + const lastConfirmed = await api.lastConfirm(); + const maxBlock = queue.sort((p, q) => q.blockNumber - p.blockNumber)[0].blockNumber; + if (lastConfirmed === maxBlock + 1) { + return; + } + + // Submit new proposal + const target = Math.max(lastConfirmed, maxBlock) + 1; + log(`Currently we have ${queue.length} txs are waiting to be redeemed`); + await api.submitProposal([await shadow.getProposal( + lastConfirmed, + target, + target - 1, + )]).catch(log.err); + + // Refresh target + submitted.push(target); + }, 60000); +} diff --git a/src/types.ts b/src/types.ts index 2718afe..84c3593 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,7 +56,13 @@ export interface IReceipt { export interface IReceiptWithProof { header: IDarwiniaEthBlock, receipt_proof: IReceipt, - mmr_proof: string[], + mmr_proof: IMMRProof, +} + +export interface IMMRProof { + member_leaf_index: number, + last_leaf_index: number, + proof: string[] } /// Proposal Header Interface @@ -64,7 +70,7 @@ export interface IProposalHeader { eth_header: IDarwiniaEthBlock, ethash_proof: IDoubleNodeWithMerkleProof[], mmr_root: string, - mmr_proof: string[] + mmr_proof: string[], } // Proposal Headers Interface diff --git a/src/util/cfg.ts b/src/util/cfg.ts index 851e460..fd49d0c 100644 --- a/src/util/cfg.ts +++ b/src/util/cfg.ts @@ -33,6 +33,7 @@ interface IEthConfig { export interface IConfig { node: string; + relayer: string; seed: string; shadow: string; eth: IEthConfig; @@ -76,6 +77,7 @@ export class Config { public eth: IEthConfig; public node: string; public path: string; + public relayer: string; public shadow: string; public types: Record; private seed: string; @@ -96,6 +98,7 @@ export class Config { const cj = Config.load(conf, rawCj); this.node = cj.node; + this.relayer = cj.relayer; this.seed = cj.seed; this.shadow = cj.shadow; this.eth = cj.eth; diff --git a/src/util/static/config.json b/src/util/static/config.json index 5fca6e4..b0a6064 100644 --- a/src/util/static/config.json +++ b/src/util/static/config.json @@ -1,26 +1,27 @@ { - "node": "wss://crab.darwinia.network", - "seed": "", - "shadow": "https://shadow.darwinia.network", - "eth": { - "RPC_SERVER": "https://ropsten.infura.io/v3/0bfb9acbb13c426097aabb1d81a9d016", - "START_BLOCK_NUMBER": 8647036, - "CONTRACT": { - "RING": { - "address": "0xb52FBE2B925ab79a821b261C82c5Ba0814AAA5e0", - "burnAndRedeemTopics": "0xc9dcda609937876978d7e0aa29857cb187aea06ad9e843fd23fd32108da73f10" - }, - "KTON": { - "address": "0x1994100c58753793D52c6f457f189aa3ce9cEe94", - "burnAndRedeemTopics": "0xc9dcda609937876978d7e0aa29857cb187aea06ad9e843fd23fd32108da73f10" - }, - "BANK": { - "address": "0x6EF538314829EfA8386Fc43386cB13B4e0A67D1e", - "burnAndRedeemTopics": "0xe77bf2fa8a25e63c1e5e29e1b2fcb6586d673931e020c4e3ffede453b830fb12" - }, - "ISSUING": { - "address": "0x49262B932E439271d05634c32978294C7Ea15d0C" - } - } + "node": "wss://crab.darwinia.network", + "relayer": "", + "seed": "", + "shadow": "https://shadow.darwinia.network", + "eth": { + "RPC_SERVER": "https://ropsten.infura.io/v3/0bfb9acbb13c426097aabb1d81a9d016", + "START_BLOCK_NUMBER": 8647036, + "CONTRACT": { + "RING": { + "address": "0xb52FBE2B925ab79a821b261C82c5Ba0814AAA5e0", + "burnAndRedeemTopics": "0xc9dcda609937876978d7e0aa29857cb187aea06ad9e843fd23fd32108da73f10" + }, + "KTON": { + "address": "0x1994100c58753793D52c6f457f189aa3ce9cEe94", + "burnAndRedeemTopics": "0xc9dcda609937876978d7e0aa29857cb187aea06ad9e843fd23fd32108da73f10" + }, + "BANK": { + "address": "0x6EF538314829EfA8386Fc43386cB13B4e0A67D1e", + "burnAndRedeemTopics": "0xe77bf2fa8a25e63c1e5e29e1b2fcb6586d673931e020c4e3ffede453b830fb12" + }, + "ISSUING": { + "address": "0x49262B932E439271d05634c32978294C7Ea15d0C" + } } + } }