From 4250aa214d3d3416c0d0c179b4072b2956c5a726 Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Tue, 7 Jan 2020 23:57:58 +0100 Subject: [PATCH 01/10] basic implementation of send offline --- packages/signer-cli/src/cmdSendOffline.ts | 73 +++++++++++++++++++++++ packages/signer-cli/src/signer.ts | 4 ++ 2 files changed, 77 insertions(+) create mode 100644 packages/signer-cli/src/cmdSendOffline.ts diff --git a/packages/signer-cli/src/cmdSendOffline.ts b/packages/signer-cli/src/cmdSendOffline.ts new file mode 100644 index 000000000..5da9ba0d5 --- /dev/null +++ b/packages/signer-cli/src/cmdSendOffline.ts @@ -0,0 +1,73 @@ +// Copyright 2018-2020 @polkadot/signer-cli authors & contributors +// This software may be modified and distributed under the terms +// of the Apache-2.0 license. See the LICENSE file for details. + +import * as readline from "readline"; +import { ApiPromise, WsProvider } from "@polkadot/api"; +import { assert } from "@polkadot/util"; + +function getSignature(data: any) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + return new Promise((resolve): void => { + rl.question(`Payload: ${data}\nSignature> `, signature => { + resolve(signature); + rl.close(); + }); + }); +} + +export default async function cmdSendOffline( + account: string, + blocks: number | undefined, + endpoint: string, + [tx, ...params]: string[] +): Promise { + const provider = new WsProvider(endpoint); + const api = await ApiPromise.create({ provider }); + const [section, method] = tx.split("."); + + assert(api.tx[section] && api.tx[section][method], `Unable to find method ${section}.${method}`); + + const options: any = {}; + let blockNumber: any; + + if (blocks === 0) { + options.era = 0; + options.blockHash = api.genesisHash; + blockNumber = 0; + } else if (blocks != null) { + // Get current block if we want to modify the number of blocks we have to sign + const signedBlock = await api.rpc.chain.getBlock(); + + options.blockHash = signedBlock.block.header.hash; + options.era = api.createType("ExtrinsicEra", { + current: signedBlock.block.header.number, + period: blocks + }); + blockNumber = signedBlock.block.header.number; + } + + const transaction: any = api.tx[section][method](...params); + + const payload: any = api.createType("SignerPayload", { + version: api.extrinsicVersion, + runtimeVersion: api.runtimeVersion, + genesisHash: api.genesisHash, + ...options, + address: account, + method: transaction.method, + blockNumber + }); + + const signature = await getSignature(payload.toRaw().data); + + transaction.addSignature(account, signature, payload.toPayload()); + + console.log("\nSigned transaction:\n" + transaction.toJSON()); + + process.exit(0); +} diff --git a/packages/signer-cli/src/signer.ts b/packages/signer-cli/src/signer.ts index 74b4b22c2..284202625 100644 --- a/packages/signer-cli/src/signer.ts +++ b/packages/signer-cli/src/signer.ts @@ -6,6 +6,7 @@ import yargs from 'yargs'; import cmdSign from './cmdSign'; import cmdSubmit from './cmdSubmit'; +import cmdSendOffline from './cmdSendOffline'; const BLOCKTIME = 6; const ONE_MINUTE = 60 / BLOCKTIME; @@ -57,6 +58,9 @@ async function main (): Promise { } else if (command === 'submit') { const mortality = minutes != null ? minutes * ONE_MINUTE : blocks; return cmdSubmit(account, mortality, ws || '', params); + } else if (command === 'sendOffline') { + const mortality = minutes != null ? minutes * ONE_MINUTE : blocks; + return cmdSendOffline(account, mortality, ws || '', params); } throw new Error(`Unknown command '${command}' found, expected one of 'sign' or 'submit'`); From ddf5c2f2145b09f2ce0429aad7bf2ad8efff253d Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Thu, 9 Jan 2020 11:45:16 +0100 Subject: [PATCH 02/10] added nonce --- packages/signer-cli/src/cmdSendOffline.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/signer-cli/src/cmdSendOffline.ts b/packages/signer-cli/src/cmdSendOffline.ts index 5da9ba0d5..3b813e837 100644 --- a/packages/signer-cli/src/cmdSendOffline.ts +++ b/packages/signer-cli/src/cmdSendOffline.ts @@ -52,6 +52,7 @@ export default async function cmdSendOffline( } const transaction: any = api.tx[section][method](...params); + const nonce: any = await api.query.system.accountNonce(account); const payload: any = api.createType("SignerPayload", { version: api.extrinsicVersion, @@ -60,7 +61,8 @@ export default async function cmdSendOffline( ...options, address: account, method: transaction.method, - blockNumber + blockNumber, + nonce }); const signature = await getSignature(payload.toRaw().data); From 86d9a2424114c6d2e89c3a169918ce895f6c23cb Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 00:04:01 +0100 Subject: [PATCH 03/10] added yargs docs and fixed typos --- packages/signer-cli/src/signer.ts | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/signer-cli/src/signer.ts b/packages/signer-cli/src/signer.ts index 284202625..4a1423747 100644 --- a/packages/signer-cli/src/signer.ts +++ b/packages/signer-cli/src/signer.ts @@ -14,7 +14,8 @@ const ONE_MINUTE = 60 / BLOCKTIME; const { _: [command, ...params], account, blocks, minutes, seed, type, ws } = yargs .usage('Usage: [options] <...params>') .usage('Example: submit --account D3AhD...wrx --ws wss://... balances.transfer F7Gh 10000 ') - .usage('Example: sign --seed "..." --account D3AhD...wrx --crypto ed25519 0x123...789') + .usage('Example: sign --seed "..." --account D3AhD...wrx --type ed25519 0x123...789') + .usage('Example: sendOffline --seed "..." --account D3AhD...wrx --type ed25519 0x123...789') .wrap(120) .options({ account: { @@ -33,12 +34,17 @@ const { _: [command, ...params], account, blocks, minutes, seed, type, ws } = ya type: 'string' }, minutes: { - description: 'Approximate time for a transction to be signed and submitted before becoming invalid (mortality in minutes)', + description: 'Approximate time for a transaction to be signed and submitted before becoming invalid (mortality in minutes)', default: undefined as number | undefined, type: 'number' }, blocks: { - description: 'Exact number of blocks for a transction to be signed and submitted before becoming invalid (mortality in blocks). Set to 0 for an immortal transaction (not recomended)', + description: 'Exact number of blocks for a transaction to be signed and submitted before becoming invalid (mortality in blocks). Set to 0 for an immortal transaction (not recomended, but useful for \'sendOffline\' command to ensure the transaction does not expire)', + default: undefined as number | undefined, + type: 'number' + }, + nonce: { + description: 'Transaction nonce (sendOffline only)', default: undefined as number | undefined, type: 'number' }, @@ -63,7 +69,7 @@ async function main (): Promise { return cmdSendOffline(account, mortality, ws || '', params); } - throw new Error(`Unknown command '${command}' found, expected one of 'sign' or 'submit'`); + throw new Error(`Unknown command '${command}' found, expected one of 'sign', 'submit' or 'sendOffline'`); } main().catch((error): void => { From 036248e9297b3b3ed5e272b616e960c55bdc364a Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 00:34:34 +0100 Subject: [PATCH 04/10] passing nonce as positional parameter --- packages/signer-cli/src/signer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signer-cli/src/signer.ts b/packages/signer-cli/src/signer.ts index 4a1423747..e1a39c7eb 100644 --- a/packages/signer-cli/src/signer.ts +++ b/packages/signer-cli/src/signer.ts @@ -11,7 +11,7 @@ import cmdSendOffline from './cmdSendOffline'; const BLOCKTIME = 6; const ONE_MINUTE = 60 / BLOCKTIME; -const { _: [command, ...params], account, blocks, minutes, seed, type, ws } = yargs +const { _: [command, ...params], account, blocks, nonce, minutes, seed, type, ws } = yargs .usage('Usage: [options] <...params>') .usage('Example: submit --account D3AhD...wrx --ws wss://... balances.transfer F7Gh 10000 ') .usage('Example: sign --seed "..." --account D3AhD...wrx --type ed25519 0x123...789') @@ -66,7 +66,7 @@ async function main (): Promise { return cmdSubmit(account, mortality, ws || '', params); } else if (command === 'sendOffline') { const mortality = minutes != null ? minutes * ONE_MINUTE : blocks; - return cmdSendOffline(account, mortality, ws || '', params); + return cmdSendOffline(account, mortality, ws || '', nonce, params); } throw new Error(`Unknown command '${command}' found, expected one of 'sign', 'submit' or 'sendOffline'`); From 2bdd9ef7629d9d97b2f0cf2077055306455d39da Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 00:35:10 +0100 Subject: [PATCH 05/10] added types --- packages/signer-cli/src/cmdSendOffline.ts | 52 +++++++++++++---------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/packages/signer-cli/src/cmdSendOffline.ts b/packages/signer-cli/src/cmdSendOffline.ts index 3b813e837..b259948bd 100644 --- a/packages/signer-cli/src/cmdSendOffline.ts +++ b/packages/signer-cli/src/cmdSendOffline.ts @@ -2,11 +2,14 @@ // This software may be modified and distributed under the terms // of the Apache-2.0 license. See the LICENSE file for details. -import * as readline from "readline"; -import { ApiPromise, WsProvider } from "@polkadot/api"; -import { assert } from "@polkadot/util"; +import * as readline from 'readline'; +import { ApiPromise, WsProvider } from '@polkadot/api'; +import { Index, SignerPayload, BlockNumber } from '@polkadot/types/interfaces'; +import { SubmittableExtrinsic, SignerOptions } from '@polkadot/api/submittable/types'; +import { Compact } from '@polkadot/types'; +import { assert } from '@polkadot/util'; -function getSignature(data: any) { +function getSignature (data: string): Promise { const rl = readline.createInterface({ input: process.stdin, output: process.stdout @@ -20,56 +23,61 @@ function getSignature(data: any) { }); } -export default async function cmdSendOffline( +export default async function cmdSendOffline ( account: string, blocks: number | undefined, endpoint: string, + nonce: number | undefined | Index, [tx, ...params]: string[] ): Promise { const provider = new WsProvider(endpoint); const api = await ApiPromise.create({ provider }); - const [section, method] = tx.split("."); + const [section, method] = tx.split('.'); assert(api.tx[section] && api.tx[section][method], `Unable to find method ${section}.${method}`); - const options: any = {}; - let blockNumber: any; + let options: SignerOptions | object = {}; + let blockNumber: Compact | number | null = null; + nonce = nonce || await api.query.system.accountNonce(account); if (blocks === 0) { - options.era = 0; - options.blockHash = api.genesisHash; + options = { + era: 0, + blockHash: api.genesisHash, + nonce + }; blockNumber = 0; } else if (blocks != null) { // Get current block if we want to modify the number of blocks we have to sign const signedBlock = await api.rpc.chain.getBlock(); - - options.blockHash = signedBlock.block.header.hash; - options.era = api.createType("ExtrinsicEra", { - current: signedBlock.block.header.number, - period: blocks - }); + options = { + blockHash: signedBlock.block.header.hash, + era: api.createType('ExtrinsicEra', { + current: signedBlock.block.header.number, + period: blocks + }), + nonce + }; blockNumber = signedBlock.block.header.number; } - const transaction: any = api.tx[section][method](...params); - const nonce: any = await api.query.system.accountNonce(account); + const transaction: SubmittableExtrinsic<'promise'> = api.tx[section][method](...params); - const payload: any = api.createType("SignerPayload", { + const payload: SignerPayload = api.createType('SignerPayload', { version: api.extrinsicVersion, runtimeVersion: api.runtimeVersion, genesisHash: api.genesisHash, ...options, address: account, method: transaction.method, - blockNumber, - nonce + blockNumber }); const signature = await getSignature(payload.toRaw().data); transaction.addSignature(account, signature, payload.toPayload()); - console.log("\nSigned transaction:\n" + transaction.toJSON()); + console.log('\nSigned transaction:\n' + transaction.toJSON()); process.exit(0); } From ab9047e143cbbb6e27af2ed00b7f2ce185ad65a1 Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 00:47:56 +0100 Subject: [PATCH 06/10] fixed nonce logic --- packages/signer-cli/src/cmdSendOffline.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/signer-cli/src/cmdSendOffline.ts b/packages/signer-cli/src/cmdSendOffline.ts index b259948bd..9c93cf0dd 100644 --- a/packages/signer-cli/src/cmdSendOffline.ts +++ b/packages/signer-cli/src/cmdSendOffline.ts @@ -36,9 +36,13 @@ export default async function cmdSendOffline ( assert(api.tx[section] && api.tx[section][method], `Unable to find method ${section}.${method}`); - let options: SignerOptions | object = {}; + if (nonce == null) { + nonce = await api.query.system.accountNonce(account); + } + let options: SignerOptions | object = { + nonce + }; let blockNumber: Compact | number | null = null; - nonce = nonce || await api.query.system.accountNonce(account); if (blocks === 0) { options = { From c5fda7719f4104ce1fbba649057e3375754ab1c5 Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 01:21:25 +0100 Subject: [PATCH 07/10] improved yargs docs --- packages/signer-cli/src/signer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/signer-cli/src/signer.ts b/packages/signer-cli/src/signer.ts index e1a39c7eb..7d7da6056 100644 --- a/packages/signer-cli/src/signer.ts +++ b/packages/signer-cli/src/signer.ts @@ -39,7 +39,7 @@ const { _: [command, ...params], account, blocks, nonce, minutes, seed, type, ws type: 'number' }, blocks: { - description: 'Exact number of blocks for a transaction to be signed and submitted before becoming invalid (mortality in blocks). Set to 0 for an immortal transaction (not recomended, but useful for \'sendOffline\' command to ensure the transaction does not expire)', + description: 'Exact number of blocks for a transaction to be signed and submitted before becoming invalid (mortality in blocks). Set to 0 for an immortal transaction (not recomended)', default: undefined as number | undefined, type: 'number' }, @@ -49,7 +49,7 @@ const { _: [command, ...params], account, blocks, nonce, minutes, seed, type, ws type: 'number' }, ws: { - description: 'The API endpoint to connect to, e.g. wss://poc3-rpc.polkadot.io (submit only)', + description: 'The API endpoint to connect to, e.g. wss://poc3-rpc.polkadot.io (submit and sendOffline only)', type: 'string' } }) From 428990f40b5ac80bbaca0edc119462964926cdc3 Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 01:22:07 +0100 Subject: [PATCH 08/10] improved logic and added default blocks --- packages/signer-cli/src/cmdSendOffline.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/signer-cli/src/cmdSendOffline.ts b/packages/signer-cli/src/cmdSendOffline.ts index 9c93cf0dd..7a9875752 100644 --- a/packages/signer-cli/src/cmdSendOffline.ts +++ b/packages/signer-cli/src/cmdSendOffline.ts @@ -36,6 +36,9 @@ export default async function cmdSendOffline ( assert(api.tx[section] && api.tx[section][method], `Unable to find method ${section}.${method}`); + if (blocks == null) { + blocks = 50; + } if (nonce == null) { nonce = await api.query.system.accountNonce(account); } @@ -47,11 +50,10 @@ export default async function cmdSendOffline ( if (blocks === 0) { options = { era: 0, - blockHash: api.genesisHash, - nonce + blockHash: api.genesisHash }; blockNumber = 0; - } else if (blocks != null) { + } else { // Get current block if we want to modify the number of blocks we have to sign const signedBlock = await api.rpc.chain.getBlock(); options = { @@ -59,8 +61,7 @@ export default async function cmdSendOffline ( era: api.createType('ExtrinsicEra', { current: signedBlock.block.header.number, period: blocks - }), - nonce + }) }; blockNumber = signedBlock.block.header.number; } From 6be09476ceea32dc3c14b1f0a35b4469fc73df71 Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 01:24:42 +0100 Subject: [PATCH 09/10] added sendOffline to readme --- packages/signer-cli/README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/signer-cli/README.md b/packages/signer-cli/README.md index 173a77706..230f89566 100644 --- a/packages/signer-cli/README.md +++ b/packages/signer-cli/README.md @@ -40,3 +40,17 @@ Signature: 0xe6facf194a8e...413ce3155c2d1240b Paste this signature into the submission in the first terminal, and off we go. By default, `submit` will create a mortal extrinsic with a lifetime of 50 blocks. Assuming a six-second block time, you will have five minutes to go offline, sign the transaction, paste the signature, and submit the signed transaction. + +## Send offline + +This functionality lets you generate signed transaction for execution at a later date. It is intended to resemble MyEtherWallet's [`Send offline`](https://kb.myetherwallet.com/en/offline/offline_transaction/) feature. + +The flow is similar to the `submit` command. First, run the `sendOffline` command on a computer with a network connection: + +`yarn run:signer sendOffline --account 5HNHXTw65dTNVGRdYkxFUpKcvmUYQMZHcDHmSKpuC8pvVEaN --ws wss://poc3-rpc.polkadot.io/ balances.transfer 5DkQbYAExs3M2sZgT1Ec3mKfZnAQCL4Dt9beTCknkCUn5jzo 123` + +This will give you a payload to sign. Use the `sign` command as per instructions above. + +Once you've pasted the signature into the `sendOffline` terminal (and hit `Enter`), it will print the signed transaction that can be stored and submitted later. + +Run `yarn run:signer --help` to learn about optional parameters. From 38a63a877dd38a72f14bc7d4406cb7eef47f82d1 Mon Sep 17 00:00:00 2001 From: Marcin Zolkiewski Date: Fri, 10 Jan 2020 02:01:25 +0100 Subject: [PATCH 10/10] support submitting pre-signed transactions --- packages/signer-cli/README.md | 10 ++++++++-- packages/signer-cli/src/cmdSubmit.ts | 21 +++++++++++++++++++-- packages/signer-cli/src/signer.ts | 15 +++++++++------ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/signer-cli/README.md b/packages/signer-cli/README.md index 230f89566..b905afeb4 100644 --- a/packages/signer-cli/README.md +++ b/packages/signer-cli/README.md @@ -21,6 +21,12 @@ Signature> The `Payload` is the hex that needs to be signed. Pasting the hex signature (followed by `Enter`) submits it to the chain. +You can also use this command to submit pre-signed transactions, e.g. generated using the `sendOffline` command (see below). + +The syntax is as follows: + +`yarn run:signer submit --tx --ws ` + ## Sign a transaction To sign, you do not need a network connection at all and this command does not use the API to make connections to a chain. In a terminal, run the `sign` command with the following form: @@ -43,7 +49,7 @@ By default, `submit` will create a mortal extrinsic with a lifetime of 50 blocks ## Send offline -This functionality lets you generate signed transaction for execution at a later date. It is intended to resemble MyEtherWallet's [`Send offline`](https://kb.myetherwallet.com/en/offline/offline_transaction/) feature. +This functionality lets you generate signed transactions for execution at a later time, on a different device or using other tools. It is intended to resemble MyEtherWallet's [`Send offline`](https://kb.myetherwallet.com/en/offline/offline_transaction/) feature. The flow is similar to the `submit` command. First, run the `sendOffline` command on a computer with a network connection: @@ -51,6 +57,6 @@ The flow is similar to the `submit` command. First, run the `sendOffline` comman This will give you a payload to sign. Use the `sign` command as per instructions above. -Once you've pasted the signature into the `sendOffline` terminal (and hit `Enter`), it will print the signed transaction that can be stored and submitted later. +Once you've pasted the signature into the `sendOffline` terminal (and hit `Enter`), it will print the signed transaction that can be stored and submitted later (e.g. using the `submit` command). Run `yarn run:signer --help` to learn about optional parameters. diff --git a/packages/signer-cli/src/cmdSubmit.ts b/packages/signer-cli/src/cmdSubmit.ts index 21c7ac8d7..4eb6e8623 100644 --- a/packages/signer-cli/src/cmdSubmit.ts +++ b/packages/signer-cli/src/cmdSubmit.ts @@ -26,11 +26,28 @@ class RawSigner implements Signer { } } -export default async function cmdSubmit (account: string, blocks: number | undefined, endpoint: string, [tx, ...params]: string[]): Promise { +function submitPreSignedTx (api: ApiPromise, tx: string): void { + const extrinsic = api.createType('Extrinsic', tx); + + api.rpc.author.submitAndWatchExtrinsic(extrinsic, result => { + console.log(JSON.stringify(result)); + + if (result.isFinalized) { + process.exit(0); + } + }); +} + +export default async function cmdSubmit (account: string, blocks: number | undefined, endpoint: string, tx: string | undefined, [txName, ...params]: string[]): Promise { const signer = new RawSigner(); const provider = new WsProvider(endpoint); const api = await ApiPromise.create({ provider, signer }); - const [section, method] = tx.split('.'); + + if (tx) { + return submitPreSignedTx(api, tx); + } + + const [section, method] = txName.split('.'); assert(api.tx[section] && api.tx[section][method], `Unable to find method ${section}.${method}`); diff --git a/packages/signer-cli/src/signer.ts b/packages/signer-cli/src/signer.ts index 7d7da6056..f6b5eb3fc 100644 --- a/packages/signer-cli/src/signer.ts +++ b/packages/signer-cli/src/signer.ts @@ -11,7 +11,7 @@ import cmdSendOffline from './cmdSendOffline'; const BLOCKTIME = 6; const ONE_MINUTE = 60 / BLOCKTIME; -const { _: [command, ...params], account, blocks, nonce, minutes, seed, type, ws } = yargs +const { _: [command, ...params], account, blocks, minutes, nonce, seed, type, ws, tx } = yargs .usage('Usage: [options] <...params>') .usage('Example: submit --account D3AhD...wrx --ws wss://... balances.transfer F7Gh 10000 ') .usage('Example: sign --seed "..." --account D3AhD...wrx --type ed25519 0x123...789') @@ -20,8 +20,7 @@ const { _: [command, ...params], account, blocks, nonce, minutes, seed, type, ws .options({ account: { description: 'The actual address for the signer', - type: 'string', - required: true + type: 'string' }, seed: { description: 'The account seed to use (sign only)', @@ -51,6 +50,10 @@ const { _: [command, ...params], account, blocks, nonce, minutes, seed, type, ws ws: { description: 'The API endpoint to connect to, e.g. wss://poc3-rpc.polkadot.io (submit and sendOffline only)', type: 'string' + }, + tx: { + description: 'Pre-signed transaction generated using e.g. the sendOffline command. If provided, only --ws is required as well (submit only)', + type: 'string' } }) .strict() @@ -60,13 +63,13 @@ const { _: [command, ...params], account, blocks, nonce, minutes, seed, type, ws // eslint-disable-next-line @typescript-eslint/require-await async function main (): Promise { if (command === 'sign') { - return cmdSign(account, seed || '', type as 'ed25519', params); + return cmdSign(account as string, seed || '', type as 'ed25519', params); } else if (command === 'submit') { const mortality = minutes != null ? minutes * ONE_MINUTE : blocks; - return cmdSubmit(account, mortality, ws || '', params); + return cmdSubmit(account as string, mortality, ws || '', tx, params); } else if (command === 'sendOffline') { const mortality = minutes != null ? minutes * ONE_MINUTE : blocks; - return cmdSendOffline(account, mortality, ws || '', nonce, params); + return cmdSendOffline(account as string, mortality, ws || '', nonce, params); } throw new Error(`Unknown command '${command}' found, expected one of 'sign', 'submit' or 'sendOffline'`);