Skip to content

Commit

Permalink
Signer CLI - send offline (#79)
Browse files Browse the repository at this point in the history
* basic implementation of send offline

* added nonce

* added yargs docs and fixed typos

* passing nonce as positional parameter

* added types

* fixed nonce logic

* improved yargs docs

* improved logic and added default blocks

* added sendOffline to readme

* support submitting pre-signed transactions
  • Loading branch information
ma12ki authored and jacogr committed Jan 10, 2020
1 parent 5b6aa3b commit 4796e4f
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 12 deletions.
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

0 comments on commit 4796e4f

Please sign in to comment.