Skip to content

Commit

Permalink
Add signedTransaction to SignerResult for injected Signers in `si…
Browse files Browse the repository at this point in the history
…gnAndSend` (#5914)

* Start work on payload feature

* Change naming and type

* Add validation and whitelisting

* Update comments

* Fix some properties and commetns

* Switch logic over to signedTransaction

* Add mode to output of extrinsic signature

* Add validation

* Update comments

* Update comments

* Reduce validation

* Update internal docs

* Make comments clearer

* comments

* one more inline doc :)

* Fix nasty little bug for signAsync, and when signedTransaction is not used

* remove uncessecary ?

* Add undefined typing
  • Loading branch information
TarikGul authored Jun 24, 2024
1 parent 5ca873b commit 586f02f
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 8 deletions.
10 changes: 10 additions & 0 deletions packages/api-base/src/types/submittable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@ export interface SubmittableExtrinsic<ApiType extends ApiTypes, R extends ISubmi

signAsync (account: AddressOrPair, _options?: Partial<SignerOptions>): PromiseOrObs<ApiType, this>;

/**
* @description Sign and broadcast the constructued transaction.
*
* Note for injected signers:
* As of v12.0.1 and up the `SignerResult` return type for `signPayload` allows for the `signedTransaction` field.
* This allows the signer to input a signed transaction that will be directly broadcasted. This
* bypasses the api adding the signature to the payload. The api will ensure that the Call Data is not changed before it broadcasts the
* transaction. This allows for the signer to modify the payload to add things like `mode`, and `metadataHash` for
* signedExtensions such as `CheckMetadataHash`.
*/
signAndSend (account: AddressOrPair, options?: Partial<SignerOptions>): SubmittableResultResult<ApiType, R>;

signAndSend (account: AddressOrPair, statusCb: Callback<R>): SubmittableResultSubscription<ApiType>;
Expand Down
58 changes: 50 additions & 8 deletions packages/api/src/submittable/createClass.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { Observable } from 'rxjs';
import type { Address, ApplyExtrinsicResult, Call, Extrinsic, ExtrinsicEra, ExtrinsicStatus, Hash, Header, Index, RuntimeDispatchInfo, SignerPayload } from '@polkadot/types/interfaces';
import type { Callback, Codec, CodecClass, ISubmittableResult, SignatureOptions } from '@polkadot/types/types';
import type { Registry } from '@polkadot/types-codec/types';
import type { HexString } from '@polkadot/util/types';
import type { ApiBase } from '../base/index.js';
import type { ApiInterfaceRx, ApiTypes, PromiseOrObs, SignerResult } from '../types/index.js';
import type { AddressOrPair, SignerOptions, SubmittableDryRunResult, SubmittableExtrinsic, SubmittablePaymentResult, SubmittableResultResult, SubmittableResultSubscription } from './types.js';
Expand All @@ -28,6 +29,12 @@ interface SubmittableOptions<ApiType extends ApiTypes> {
interface UpdateInfo {
options: SignatureOptions;
updateId: number;
signedTransaction: HexString | Uint8Array | null;
}

interface SignerInfo {
id: number;
signedTransaction?: HexString | Uint8Array;
}

function makeEraOptions (api: ApiInterfaceRx, registry: Registry, partialOptions: Partial<SignerOptions>, { header, mortalLength, nonce }: { header: Header | null; mortalLength: number; nonce: Index }): SignatureOptions {
Expand Down Expand Up @@ -246,14 +253,21 @@ export function createClass <ApiType extends ApiTypes> ({ api, apiType, blockHas
mergeMap(async (signingInfo): Promise<UpdateInfo> => {
const eraOptions = makeEraOptions(api, this.registry, options, signingInfo);
let updateId = -1;
let signedTx = null;

if (isKeyringPair(account)) {
this.sign(account, eraOptions);
} else {
updateId = await this.#signViaSigner(address, eraOptions, signingInfo.header);
const result = await this.#signViaSigner(address, eraOptions, signingInfo.header);

updateId = result.id;

if (result.signedTransaction) {
signedTx = result.signedTransaction;
}
}

return { options: eraOptions, updateId };
return { options: eraOptions, signedTransaction: signedTx, updateId };
})
);
};
Expand Down Expand Up @@ -288,18 +302,18 @@ export function createClass <ApiType extends ApiTypes> ({ api, apiType, blockHas
);
};

#observeSend = (info: UpdateInfo): Observable<Hash> => {
return api.rpc.author.submitExtrinsic(this).pipe(
#observeSend = (info?: UpdateInfo): Observable<Hash> => {
return api.rpc.author.submitExtrinsic(info?.signedTransaction || this).pipe(
tap((hash): void => {
this.#updateSigner(hash, info);
})
);
};

#observeSubscribe = (info: UpdateInfo): Observable<ISubmittableResult> => {
#observeSubscribe = (info?: UpdateInfo): Observable<ISubmittableResult> => {
const txHash = this.hash;

return api.rpc.author.submitAndWatchExtrinsic(this).pipe(
return api.rpc.author.submitAndWatchExtrinsic(info?.signedTransaction || this).pipe(
switchMap((status): Observable<ISubmittableResult> =>
this.#observeStatus(txHash, status)
),
Expand All @@ -309,7 +323,7 @@ export function createClass <ApiType extends ApiTypes> ({ api, apiType, blockHas
);
};

#signViaSigner = async (address: Address | string | Uint8Array, options: SignatureOptions, header: Header | null): Promise<number> => {
#signViaSigner = async (address: Address | string | Uint8Array, options: SignatureOptions, header: Header | null): Promise<SignerInfo> => {
const signer = options.signer || api.signer;

if (!signer) {
Expand All @@ -325,6 +339,20 @@ export function createClass <ApiType extends ApiTypes> ({ api, apiType, blockHas

if (isFunction(signer.signPayload)) {
result = await signer.signPayload(payload.toPayload());

// When the signedTransaction is included by the signer, we no longer add
// the signature to the parent class, but instead broadcast the signed transaction directly.
if (result.signedTransaction) {
const ext = this.registry.createTypeUnsafe<Extrinsic>('Extrinsic', [result.signedTransaction]);

if (!ext.isSigned) {
throw new Error(`When using the signedTransaction field, the transaction must be signed. Recieved isSigned: ${ext.isSigned}`);
}

this.#validateSignedTransaction(payload, ext);

return { id: result.id, signedTransaction: result.signedTransaction };
}
} else if (isFunction(signer.signRaw)) {
result = await signer.signRaw(payload.toRaw());
} else {
Expand All @@ -336,7 +364,7 @@ export function createClass <ApiType extends ApiTypes> ({ api, apiType, blockHas
// payload data is not modified from our inputs, but the signer
super.addSignature(address, result.signature, payload.toPayload());

return result.id;
return { id: result.id };
};

#updateSigner = (status: Hash | ISubmittableResult, info?: UpdateInfo): void => {
Expand All @@ -349,6 +377,20 @@ export function createClass <ApiType extends ApiTypes> ({ api, apiType, blockHas
}
}
};

/**
* When a signer includes `signedTransaction` within the SignerResult this will validate
* specific fields within the signed extrinsic against the original payload that was passed
* to the signer.
*/
#validateSignedTransaction = (signerPayload: SignerPayload, signedExt: Extrinsic): void => {
const payload = signerPayload.toPayload();
const errMsg = (field: string) => `signAndSend: ${field} does not match the original payload`;

if (payload.method !== signedExt.method.toHex()) {
throw new Error(errMsg('call data'));
}
};
}

return Submittable;
Expand Down
8 changes: 8 additions & 0 deletions packages/types/src/extrinsic/Extrinsic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,13 @@ abstract class ExtrinsicBase<A extends AnyTuple> extends AbstractBase<ExtrinsicV
return this.inner.signature.metadataHash;
}

/**
* @description Forward compat
*/
public get mode (): INumber {
return this.inner.signature.mode;
}

/**
* @description Returns the raw transaction version (not flagged with signing information)
*/
Expand Down Expand Up @@ -334,6 +341,7 @@ export class GenericExtrinsic<A extends AnyTuple = AnyTuple> extends ExtrinsicBa
assetId: this.assetId ? this.assetId.toHuman(isExpanded, disableAscii) : null,
era: this.era.toHuman(isExpanded, disableAscii),
metadataHash: this.metadataHash ? this.metadataHash.toHex() : null,
mode: this.mode.toHuman(),
nonce: this.nonce.toHuman(isExpanded, disableAscii),
signature: this.signature.toHex(),
signer: this.signer.toHuman(isExpanded, disableAscii),
Expand Down
7 changes: 7 additions & 0 deletions packages/types/src/extrinsic/v4/ExtrinsicSignature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ export class GenericExtrinsicSignatureV4 extends Struct implements IExtrinsicSig
return this.getT('assetId');
}

/**
* @description the [[u32]] mode
*/
public get mode (): INumber {
return this.getT('mode');
}

/**
* @description The [[Hash]] for the metadata
*/
Expand Down
10 changes: 10 additions & 0 deletions packages/types/src/types/extrinsic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ export interface SignerResult {
* @description The resulting signature in hex
*/
signature: HexString;

/**
* @description The payload constructed by the signer. This allows the
* inputted signed transaction to bypass `signAndSend` from adding the signature to the payload,
* and instead broadcasting the transaction directly. There is a small validation layer. Please refer
* to the implementation for more information. If the inputted signed transaction is not actually signed, it will fail with an error.
*
* NOTE: This is only implemented for `signPayload`.
*/
signedTransaction?: HexString | Uint8Array;
}

export interface Signer {
Expand Down

0 comments on commit 586f02f

Please sign in to comment.