Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Signer CLI - send offline #79

Merged
merged 10 commits into from
Jan 10, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions packages/signer-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <signedTransaction> --ws <endpoint>`

## 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:
Expand All @@ -40,3 +46,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 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:

`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 (e.g. using the `submit` command).

Run `yarn run:signer --help` to learn about optional parameters.
88 changes: 88 additions & 0 deletions packages/signer-cli/src/cmdSendOffline.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// 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 { 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: string): Promise<string> {
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,
nonce: number | undefined | Index,
[tx, ...params]: string[]
): Promise<void> {
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}`);

if (blocks == null) {
blocks = 50;
}
if (nonce == null) {
nonce = await api.query.system.accountNonce(account);
}
let options: SignerOptions | object = {
nonce
};
let blockNumber: Compact<BlockNumber> | number | null = null;

if (blocks === 0) {
options = {
era: 0,
blockHash: api.genesisHash
};
blockNumber = 0;
} 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 = {
blockHash: signedBlock.block.header.hash,
era: api.createType('ExtrinsicEra', {
current: signedBlock.block.header.number,
period: blocks
})
};
blockNumber = signedBlock.block.header.number;
}

const transaction: SubmittableExtrinsic<'promise'> = api.tx[section][method](...params);

const payload: SignerPayload = 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);
}
21 changes: 19 additions & 2 deletions packages/signer-cli/src/cmdSubmit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,28 @@ class RawSigner implements Signer {
}
}

export default async function cmdSubmit (account: string, blocks: number | undefined, endpoint: string, [tx, ...params]: string[]): Promise<void> {
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<void> {
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}`);

Expand Down
33 changes: 23 additions & 10 deletions packages/signer-cli/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,21 @@ import yargs from 'yargs';

import cmdSign from './cmdSign';
import cmdSubmit from './cmdSubmit';
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, minutes, nonce, seed, type, ws, tx } = yargs
.usage('Usage: [options] <endpoint> <...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: {
description: 'The actual address for the signer',
type: 'string',
required: true
type: 'string'
},
seed: {
description: 'The account seed to use (sign only)',
Expand All @@ -32,17 +33,26 @@ 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)',
default: undefined as number | undefined,
type: 'number'
},
nonce: {
description: 'Transaction nonce (sendOffline only)',
default: undefined as number | undefined,
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'
},
tx: {
description: 'Pre-signed transaction generated using e.g. the sendOffline command. If provided, only --ws is required as well (submit only)',
type: 'string'
}
})
Expand All @@ -53,13 +63,16 @@ const { _: [command, ...params], account, blocks, minutes, seed, type, ws } = ya
// eslint-disable-next-line @typescript-eslint/require-await
async function main (): Promise<void> {
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 as string, mortality, ws || '', nonce, 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 => {
Expand Down