|
| 1 | +--- |
| 2 | +title: Transaction Construction |
| 3 | +description: Understand how to construct, sign, and broadcast transactions in the Polkadot ecosystem using various tools and libraries. |
| 4 | +--- |
| 5 | + |
| 6 | +# Transaction Construction |
| 7 | + |
| 8 | +## Introduction |
| 9 | + |
| 10 | +This page will discuss the transaction format in Polkadot and how to create, sign, and broadcast |
| 11 | +transactions. Like the other pages in this guide, this page demonstrates some of the available |
| 12 | +tools. |
| 13 | + |
| 14 | +**Always refer to each tool's documentation when integrating.** |
| 15 | + |
| 16 | +!!!note "Complementary Reading" |
| 17 | + Learn about the basics of [blocks, transactions, and fees](/polkadot-protocol/parachain-basics/blocks-transactions-fees/). |
| 18 | + |
| 19 | +## Transaction Format |
| 20 | + |
| 21 | +Polkadot has some basic transaction information that is common to all transactions. |
| 22 | + |
| 23 | +- Address: The SS58-encoded address of the sending account. |
| 24 | +- Block Hash: The hash of the [checkpoint](/polkadot-protocol/parachain-basics/blocks-transactions-fees/transactions/#transaction-mortality) block. |
| 25 | +- Block Number: The number of the checkpoint block. |
| 26 | +- Genesis Hash: The genesis hash of the chain. |
| 27 | +- Metadata: The SCALE-encoded metadata for the runtime when submitted. |
| 28 | +- Nonce: The nonce for this transaction.\* |
| 29 | +- Spec Version: The current spec version for the runtime. |
| 30 | +- Transaction Version: The current version for transaction format. |
| 31 | +- Tip: Optional, the [tip](https://docs.polkadot.com/polkadot-protocol/parachain-basics/blocks-transactions-fees/fees/#how-fees-are-calculated) to increase transaction priority. |
| 32 | +- Mode: The flag indicating whether to verify the metadata hash or not. |
| 33 | +- Era Period: Optional, the number of blocks after the checkpoint for which a transaction is valid. |
| 34 | + If zero, the transaction is [immortal](/polkadot-protocol/parachain-basics/blocks-transactions-fees/transactions/#transaction-mortality) |
| 35 | +- MetadataHash: Optional, the metadata hash which should match the RUNTIME_METADATA_HASH environment |
| 36 | + variable. |
| 37 | + |
| 38 | +!!!warning |
| 39 | + There are risks to making a transaction immortal. If an account is reaped and a user re-funds the |
| 40 | + account, then they could replay an immortal transaction. Always default to using a mortal extrinsic. |
| 41 | + |
| 42 | +The nonce queried from the System module does not account for pending transactions. You must track |
| 43 | +and increment the nonce manually if you want to submit multiple valid transactions at the same time. |
| 44 | + |
| 45 | +Each transaction will have its own (or no) parameters to add. For example, the `transferKeepAlive` |
| 46 | +function from the Balances pallet will take: |
| 47 | + |
| 48 | +- `dest`: Destination address |
| 49 | +- `#[compact] value`: Number of tokens (compact encoding) |
| 50 | + |
| 51 | +Refer to [the protocol specifications](https://spec.polkadot.network/id-extrinsics), for the |
| 52 | +concrete specifications and types to build a transaction. |
| 53 | + |
| 54 | +**Mode and MetadataHash** |
| 55 | + |
| 56 | +The mode and metadataHash fields were introduced in transaction construction to support the optional |
| 57 | +[`CheckMetadataHash` Signed Extension](https://github.com/polkadot-fellows/RFCs/blob/main/text/0078-merkleized-metadata.md). |
| 58 | +This enables trustless metadata verification by allowing the chain to verify the correctness of the |
| 59 | +metadata used without the need of a trusted party. This functionality was included in |
| 60 | +[v1.2.5](https://github.com/polkadot-fellows/runtimes/releases/tag/v1.2.5) runtime release by the |
| 61 | +Fellowship. A user may up out of this functionality by setting the mode to `0`. When the mode is 00, |
| 62 | +the `metadataHash` field is empty/None. |
| 63 | + |
| 64 | +**Serialized transactions and metadata** |
| 65 | + |
| 66 | +Before being submitted, transactions are serialized. Serialized transactions are hex encoded |
| 67 | +SCALE-encoded bytes. The relay chain runtimes are upgradable and therefore any interfaces are |
| 68 | +subject to change, the metadata allows developers to structure any extrinsics or storage entries |
| 69 | +accordingly. The metadata provides you with all of the information required to know how to construct |
| 70 | +the serialized call data specific to your transaction. You can read more about the metadata, its |
| 71 | +format and how to get it in the [Subxt documentation](/polkadot-protocol/parachain-basics/chain-data/#use-subxt). |
| 72 | + |
| 73 | +**Transaction Flow** |
| 74 | + |
| 75 | +The typical transaction workflow is as follows: |
| 76 | + |
| 77 | +1. Construct an unsigned transaction. |
| 78 | +2. Create a signing payload. |
| 79 | +3. Sign the payload. |
| 80 | +4. Serialize the signed payload into a transaction. |
| 81 | +5. Submit the serialized transaction. |
| 82 | + |
| 83 | +Parity provides the following tools to help perform these steps. |
| 84 | + |
| 85 | +## Polkadot-JS Tools |
| 86 | + |
| 87 | +[Polkadot-JS Tools](https://github.com/polkadot-js/tools) contains a set of command line tools for |
| 88 | +interacting with a Substrate client, including one called "Signer CLI" to create, sign, and |
| 89 | +broadcast transactions. |
| 90 | + |
| 91 | +This example will use the `signer submit` command, which will create and submit the transaction. The |
| 92 | +`signer sendOffline` command has the exact same API, but will not broadcast the transaction. |
| 93 | +`submit` and `sendOffline` must be connected to a node to fetch the current metadata and construct a |
| 94 | +valid transaction. Their API has the format: |
| 95 | + |
| 96 | +```bash |
| 97 | +yarn run:signer <submit|sendOffline> --account <from-account-ss58> --ws <endpoint> <module.method> [param1] [...] [paramX] |
| 98 | +``` |
| 99 | + |
| 100 | +Signing: |
| 101 | + |
| 102 | +```bash |
| 103 | +yarn run:signer sign --account <from-account-ss58> --seed <seed> --type <sr25519|ed25519> <payload> |
| 104 | +``` |
| 105 | + |
| 106 | +For example, let's send 0.5 DOT from `121X5bEgTZcGQx5NZjwuTjqqKoiG8B2wEAvrUFjuw24ZGZf2` to |
| 107 | +`15vrtLsCQFG3qRYUcaEeeEih4JwepocNJHkpsrqojqnZPc2y`. |
| 108 | + |
| 109 | +```bash |
| 110 | +yarn run:signer submit --account 121X5bEgTZcGQx5NZjwuTjqqKoiG8B2wEAvrUFjuw24ZGZf2 --ws ws://127.0.0.1:9944 balances.transferKeepAlive 15vrtLsCQFG3qRYUcaEeeEih4JwepocNJHkpsrqojqnZPc2y 5000000000 |
| 111 | +``` |
| 112 | + |
| 113 | +This will return a payload to sign and an input waiting for a signature. Take this payload and use |
| 114 | +your normal signing environment (e.g. air gapped machine, VM, etc.). Sign the payload: |
| 115 | + |
| 116 | +```bash |
| 117 | +yarn run:signer sign --account 121X5bEgTZcGQx5NZjwuTjqqKoiG8B2wEAvrUFjuw24ZGZf2 --seed "pulp gaze fuel ... mercy inherit equal" --type sr25519 0x040300ff4a83f1...a8239139ff3ff7c3f6 |
| 118 | +``` |
| 119 | + |
| 120 | +Save the output and bring it to the machine that you will broadcast from, enter it into `submit`'s |
| 121 | +signature field, and send the transaction (or just return the serialized transaction if using `sendOffline`). |
| 122 | + |
| 123 | +## TxWrapper |
| 124 | + |
| 125 | +If you do not want to use the CLI for signing operations, Parity provides an SDK called |
| 126 | +[TxWrapper Core](https://github.com/paritytech/txwrapper-core) to generate and sign transactions |
| 127 | +offline. For Polkadot, Kusama, and select parachains, use the `txwrapper-polkadot` package. Other |
| 128 | +Substrate-based chains will have their own `txwrapper-{chain}` implementations. See the |
| 129 | +[examples](https://github.com/paritytech/txwrapper-core/blob/main/packages/txwrapper-examples/README.md) |
| 130 | +for a guide. |
| 131 | + |
| 132 | +**Import a private key** |
| 133 | + |
| 134 | +```ts |
| 135 | +import { importPrivateKey } from '@substrate/txwrapper-polkadot'; |
| 136 | + |
| 137 | +const keypair = importPrivateKey(“pulp gaze fuel ... mercy inherit equal”); |
| 138 | +``` |
| 139 | + |
| 140 | +**Derive an address from a public key** |
| 141 | + |
| 142 | +```ts |
| 143 | +import { deriveAddress } from '@substrate/txwrapper-polkadot'; |
| 144 | + |
| 145 | +// Public key, can be either hex string, or Uint8Array |
| 146 | +const publicKey = “0x2ca17d26ca376087dc30ed52deb74bf0f64aca96fe78b05ec3e720a72adb1235”; |
| 147 | +const address = deriveAddress(publicKey); |
| 148 | +``` |
| 149 | + |
| 150 | +**Construct a transaction offline** |
| 151 | + |
| 152 | +```ts |
| 153 | +import { methods } from "@substrate/txwrapper-polkadot"; |
| 154 | + |
| 155 | +const unsigned = methods.balances.transferKeepAlive( |
| 156 | + { |
| 157 | + dest: "15vrtLsCQFG3qRYUcaEeeEih4JwepocNJHkpsrqojqnZPc2y", |
| 158 | + value: 5000000000, |
| 159 | + }, |
| 160 | + { |
| 161 | + address: "121X5bEgTZcGQx5NZjwuTjqqKoiG8B2wEAvrUFjuw24ZGZf2", |
| 162 | + blockHash: "0x1fc7493f3c1e9ac758a183839906475f8363aafb1b1d3e910fe16fab4ae1b582", |
| 163 | + blockNumber: 4302222, |
| 164 | + genesisHash: "0xe3777fa922cafbff200cadeaea1a76bd7898ad5b89f7848999058b50e715f636", |
| 165 | + metadataRpc, // must import from client RPC call state_getMetadata |
| 166 | + nonce: 2, |
| 167 | + specVersion: 1019, |
| 168 | + tip: 0, |
| 169 | + eraPeriod: 64, // number of blocks from checkpoint that transaction is valid |
| 170 | + transactionVersion: 1, |
| 171 | + }, |
| 172 | + { |
| 173 | + metadataRpc, |
| 174 | + registry, // Type registry |
| 175 | + } |
| 176 | +); |
| 177 | +``` |
| 178 | + |
| 179 | +**Construct a signing payload** |
| 180 | + |
| 181 | +```ts |
| 182 | +import { methods, createSigningPayload } from '@substrate/txwrapper-polkadot'; |
| 183 | + |
| 184 | +// See "Construct a transaction offline" for "{...}" |
| 185 | +const unsigned = methods.balances.transferKeepAlive({...}, {...}, {...}); |
| 186 | +const signingPayload = createSigningPayload(unsigned, { registry }); |
| 187 | +``` |
| 188 | + |
| 189 | +**Serialize a signed transaction** |
| 190 | + |
| 191 | +```ts |
| 192 | +import { createSignedTx } from "@substrate/txwrapper-polkadot"; |
| 193 | + |
| 194 | +// Example code, replace `signWithAlice` with actual remote signer. |
| 195 | +// An example is given here: |
| 196 | +// https://github.com/paritytech/txwrapper-core/blob/b213cabf50f18f0fe710817072a81596e1a53cae/packages/txwrapper-core/src/test-helpers/signWithAlice.ts |
| 197 | +const signature = await signWithAlice(signingPayload); |
| 198 | +const signedTx = createSignedTx(unsigned, signature, { metadataRpc, registry }); |
| 199 | +``` |
| 200 | + |
| 201 | +**Decode payload types** |
| 202 | + |
| 203 | +You may want to decode payloads to verify their contents prior to submission. |
| 204 | + |
| 205 | +```ts |
| 206 | +import { decode } from "@substrate/txwrapper-polkadot"; |
| 207 | + |
| 208 | +// Decode an unsigned tx |
| 209 | +const txInfo = decode(unsigned, { metadataRpc, registry }); |
| 210 | + |
| 211 | +// Decode a signing payload |
| 212 | +const txInfo = decode(signingPayload, { metadataRpc, registry }); |
| 213 | + |
| 214 | +// Decode a signed tx |
| 215 | +const txInfo = decode(signedTx, { metadataRpc, registry }); |
| 216 | +``` |
| 217 | + |
| 218 | +**Check a transaction's hash** |
| 219 | + |
| 220 | +```ts |
| 221 | +import { getTxHash } from ‘@substrate/txwrapper-polkadot’; |
| 222 | +const txHash = getTxHash(signedTx); |
| 223 | +``` |
| 224 | + |
| 225 | +## Submitting a Signed Payload |
| 226 | + |
| 227 | +There are several ways to submit a signed payload: |
| 228 | + |
| 229 | +1. Signer CLI (`yarn run:signer submit --tx <signed-transaction> --ws <endpoint>`) |
| 230 | +1. [Substrate API Sidecar](https://docs.polkadot.com/develop/toolkit/api-libraries/sidecar/#sidecar-api) |
| 231 | +1. [RPC](https://docs.polkadot.com/develop/toolkit/api-libraries/) with `author_submitExtrinsic` or |
| 232 | + `author_submitAndWatchExtrinsic`, the latter of which will subscribe you to events to be notified |
| 233 | + as a transaction gets validated and included in the chain. |
| 234 | + |
| 235 | +## Example Addresses |
| 236 | + |
| 237 | +Some addresses to use in the examples. See |
| 238 | +[Subkey documentation](https://docs.polkadot.com/polkadot-protocol/basics/accounts/#using-subkey). |
| 239 | + |
| 240 | +```bash |
| 241 | +$ subkey --network polkadot generate |
| 242 | +Secret phrase `pulp gaze fuel ... mercy inherit equal` is account: |
| 243 | + Secret seed: 0x57450b3e09ba4598 ... ... ... ... ... ... ... .. 219756eeba80bb16 |
| 244 | + Public key (hex): 0x2ca17d26ca376087dc30ed52deb74bf0f64aca96fe78b05ec3e720a72adb1235 |
| 245 | + Account ID: 0x2ca17d26ca376087dc30ed52deb74bf0f64aca96fe78b05ec3e720a72adb1235 |
| 246 | + SS58 Address: 121X5bEgTZcGQx5NZjwuTjqqKoiG8B2wEAvrUFjuw24ZGZf2 |
| 247 | + |
| 248 | +$ subkey --network polkadot generate |
| 249 | +Secret phrase `exercise auction soft ... obey control easily` is account: |
| 250 | + Secret seed: 0x5f4bbb9fbb69261a ... ... ... ... ... ... ... .. 4691ed7d1130fbbd |
| 251 | + Public key (hex): 0xda04de6cd781c98acf0693dfb97c11011938ad22fcc476ed0089ac5aec3fe243 |
| 252 | + Account ID: 0xda04de6cd781c98acf0693dfb97c11011938ad22fcc476ed0089ac5aec3fe243 |
| 253 | + SS58 Address: 15vrtLsCQFG3qRYUcaEeeEih4JwepocNJHkpsrqojqnZPc2y |
| 254 | +``` |
0 commit comments