diff --git a/packages/api/src/Base.ts b/packages/api/src/Base.ts index ab4024c72d..993b9e4c3b 100644 --- a/packages/api/src/Base.ts +++ b/packages/api/src/Base.ts @@ -13,7 +13,7 @@ import { HashResult, U64Result, OnCallDefinition, OnCallFunction, QueryableModuleStorage, QueryableStorage, QueryableStorageFunction, - SubmittableExtrinsicFunction, SubmittableExtrinsics, SubmittableModuleExtrinsics + SubmittableExtrinsicFunction, SubmittableExtrinsics, SubmittableModuleExtrinsics, Signer } from './types'; import EventEmitter from 'eventemitter3'; @@ -107,6 +107,7 @@ export default abstract class ApiBase implement this._rpcRx = new RpcRx(thisProvider); this._rpc = this.decorateRpc(this._rpcRx, this.onCall); this._rx.rpc = this.decorateRpc(this._rpcRx, rxOnCall); + this._rx.signer = options.signer; // we only re-register the types (global) if this is not a cloned instance if (!options.source && options.types) { @@ -157,6 +158,13 @@ export default abstract class ApiBase implement return this._type; } + /** + * @description Set an external signer which will be used to sign extrinsic when account passed in is not KeyringPair + */ + setSigner (signer: Signer) { + this._rx.signer = signer; + } + /** * @description Derived results that are injected into the API, allowing for combinations of various query results. * diff --git a/packages/api/src/SubmittableExtrinsic.ts b/packages/api/src/SubmittableExtrinsic.ts index 3b192c866c..db1fac89ac 100644 --- a/packages/api/src/SubmittableExtrinsic.ts +++ b/packages/api/src/SubmittableExtrinsic.ts @@ -4,13 +4,13 @@ import { KeyringPair } from '@polkadot/keyring/types'; import { Struct, Vector } from '@polkadot/types/codec'; -import { Extrinsic, ExtrinsicStatus, EventRecord, Hash, Index, Method, RuntimeVersion, SignedBlock, Text } from '@polkadot/types/index'; +import { AccountId, Address, Extrinsic, ExtrinsicStatus, EventRecord, Hash, Index, Method, RuntimeVersion, SignedBlock, Text } from '@polkadot/types/index'; import { AnyNumber, AnyU8a, CodecCallback } from '@polkadot/types/types'; -import { ApiInterface$Rx, ApiType, OnCallDefinition } from './types'; +import { ApiInterface$Rx, ApiType, OnCallDefinition, Signer } from './types'; import { Observable, of, combineLatest } from 'rxjs'; -import { first, map, switchMap } from 'rxjs/operators'; -import { isBn, isFunction, isNumber, isUndefined } from '@polkadot/util'; +import { first, map, mergeMap, switchMap } from 'rxjs/operators'; +import { isBn, isFunction, isNumber, isUndefined, assert } from '@polkadot/util'; import filterEvents from './util/filterEvents'; @@ -151,9 +151,9 @@ export default class SubmittableExtrinsic exten return this; } - signAndSend (account: KeyringPair, options?: Partial): SumbitableResultResult; - signAndSend (account: KeyringPair, statusCb: StatusCb): SumbitableResultSubscription; - signAndSend (account: KeyringPair, _options?: Partial | StatusCb, statusCb?: StatusCb): SumbitableResultResult | SumbitableResultSubscription { + signAndSend (account: KeyringPair | string | AccountId | Address, options?: Partial): SumbitableResultResult; + signAndSend (account: KeyringPair | string | AccountId | Address, statusCb: StatusCb): SumbitableResultSubscription; + signAndSend (account: KeyringPair | string | AccountId | Address, _options?: Partial | StatusCb, statusCb?: StatusCb): SumbitableResultResult | SumbitableResultSubscription { let options: Partial = {}; if (isFunction(_options)) { @@ -163,25 +163,37 @@ export default class SubmittableExtrinsic exten } const isSubscription = this._noStatusCb || !!statusCb; - + const isKeyringPair = isFunction((account as KeyringPair).address) && isFunction((account as KeyringPair).sign); + const address = isKeyringPair ? (account as KeyringPair).address() : account.toString(); return this._onCall( () => ( isUndefined(options.nonce) - ? this._api.query.system.accountNonce(account.address()) as Observable + ? this._api.query.system.accountNonce(address) as Observable : of(new Index(options.nonce)) - ).pipe( - first(), - switchMap((nonce) => { - this.sign(account, { ...options, nonce }); - - return isSubscription - ? this.subscribeObservable() - : this.sendObservable() as any; // ??? - }) - ), - [], - statusCb as CodecCallback, - isSubscription + ).pipe( + first(), + mergeMap(async (nonce) => { + if (isKeyringPair) { + this.sign(account as KeyringPair, { ...options, nonce }); + } else { + assert(this._api.signer, 'no signer exists'); + await (this._api.signer as Signer).sign(this, address, { + blockHash: options.blockHash || this._api.genesisHash, + version: options.version || this._api.runtimeVersion, + era: options.era, + nonce + }); + } + }), + switchMap(() => { + return isSubscription + ? this.subscribeObservable() + : this.sendObservable() as any; // ??? + }) + ), + [], + statusCb as CodecCallback, + isSubscription ) as unknown as SumbitableResultSubscription; } } diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 0f2ca54ddd..e64d18ef5e 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -5,7 +5,8 @@ import { Observable } from 'rxjs'; import { ProviderInterface } from '@polkadot/rpc-provider/types'; import { RpcRxInterface$Events } from '@polkadot/rpc-rx/types'; -import { Hash, Metadata, RuntimeVersion, u64 as U64 } from '@polkadot/types/index'; +import { Hash, Metadata, RuntimeVersion, u64 as U64, Extrinsic } from '@polkadot/types/index'; +import { SignatureOptions } from '@polkadot/types/ExtrinsicSignature'; import { CodecArg, CodecCallback, Constructor } from '@polkadot/types/types'; import { MethodFunction } from '@polkadot/types/Method'; import { StorageFunction } from '@polkadot/types/StorageKey'; @@ -109,6 +110,10 @@ export interface ApiOptions { * uses types not available in the base Substrate runtime. */ types?: { [name: string]: Constructor }; + /** + * @description An external signer which will be used to sign extrinsic when account passed in is not KeyringPair + */ + signer?: Signer; } export interface ApiInterface$Decorated { @@ -120,6 +125,7 @@ export interface ApiInterface$Decorated { query: QueryableStorage; rpc: DecoratedRpc; tx: SubmittableExtrinsics; + signer?: Signer; } export type ApiInterface$Rx = ApiInterface$Decorated; @@ -134,3 +140,7 @@ export interface ApiBaseInterface extends Reado on: (type: ApiInterface$Events, handler: (...args: Array) => any) => this; once: (type: ApiInterface$Events, handler: (...args: Array) => any) => this; } + +export interface Signer { + sign (extrinsic: Extrinsic, address: string, opt: SignatureOptions): Promise; +} diff --git a/packages/api/test/e2e/promise-tx.spec.js b/packages/api/test/e2e/promise-tx.spec.js index b7a65586c0..eb02e17d22 100644 --- a/packages/api/test/e2e/promise-tx.spec.js +++ b/packages/api/test/e2e/promise-tx.spec.js @@ -6,6 +6,7 @@ import testingPairs from '@polkadot/keyring/testingPairs'; import { randomAsHex } from '@polkadot/util-crypto'; import Api from '../../src/promise'; +import SingleAccountSigner from "../util/SingleAccountSigner"; const keyring = testingPairs(); @@ -94,6 +95,48 @@ describe.skip('e2e transactions', () => { }); }); + it('makes a transfer (signAndSend via Signer)', async (done) => { + const signer = new SingleAccountSigner(keyring.dave); + api.setSigner(signer); + return api.tx.balances + .transfer('12ghjsRJpeJpUQaCQeHcBv9pRQA3tdcMxeL8cVk9JHWJGHjd', 12345) + .signAndSend(keyring.dave.address(), ({ events, status, type }) => { + console.log('Transaction status:', type); + + if (type === 'Finalised') { + console.log('Completed at block hash', status.value.toHex()); + console.log('Events:'); + + events.forEach(({ phase, event: { data, method, section } }) => { + console.log('\t', phase.toString(), `: ${section}.${method}`, data.toString()); + }); + + if (events.length) { + done(); + } + } + }); + }); + + it('makes a transfer (signAndSend via Signer) - sad path', async () => { + //no signer + api.setSigner(); + await expect(api.tx.balances + .transfer('12ghjsRJpeJpUQaCQeHcBv9pRQA3tdcMxeL8cVk9JHWJGHjd', 12345) + .signAndSend(keyring.alice.address())).rejects.toThrow('no signer exists'); + const signer = new SingleAccountSigner(keyring.dave); + api.setSigner(signer); + //no callback + await expect(api.tx.balances + .transfer('12ghjsRJpeJpUQaCQeHcBv9pRQA3tdcMxeL8cVk9JHWJGHjd', 12345) + .signAndSend(keyring.alice.address())).rejects.toThrow('does not have the keyringPair'); + //with callback + await expect(api.tx.balances + .transfer('12ghjsRJpeJpUQaCQeHcBv9pRQA3tdcMxeL8cVk9JHWJGHjd', 12345) + .signAndSend(keyring.alice.address(), ({ events, status, type }) => { + })).rejects.toThrow('does not have the keyringPair'); + }); + it('makes a transfer (no callback)', async () => { const hash = await api.tx.balances .transfer('12ghjsRJpeJpUQaCQeHcBv9pRQA3tdcMxeL8cVk9JHWJGHjd', 12345) diff --git a/packages/api/test/util/SingleAccountSigner.js b/packages/api/test/util/SingleAccountSigner.js new file mode 100644 index 0000000000..e808fc5043 --- /dev/null +++ b/packages/api/test/util/SingleAccountSigner.js @@ -0,0 +1,13 @@ + +export default class SingleAccountSigner{ + constructor(keyringPair){ + this.keyringPair = keyringPair; + } + + async sign(extrinsic, address, opt) { + if(!this.keyringPair || String(address) !== this.keyringPair.address()){ + throw new Error('does not have the keyringPair'); + } + extrinsic.sign(this.keyringPair, opt); + } +}