Sign the Transaction Offline
This article covers some crucial details on the offline transaction signing flow, such as new extended
request parameter with additional metadata.
For Polkadot transaction signing instructions, refer to the transaction signing section.
Extended metadata
All Polkadot endpoints, generating staking transactions, now support an optional boolean parameter extended
. When it set to true
, the response includes additional metadata, which can be required for offline transaction signing and external processing.
Examples
Example request with extended: true
to /api/v1/polkadot/{network}/staking/bond:
curl --request POST \
--url https://api.p2p.org/api/v1/polkadot/westend/staking/bond \
--header 'accept: application/json' \
--header 'authorization: Bearer <token>' \
--header 'content-type: application/json' \
--data '
{
"stashAccountAddress": "5Eh1C69jwNgBPtp2oKxJ7Zy5dS6LJn3XZ6CJHPwcAB2iFpZQ",
"rewardDestinationType": "account",
"rewardDestination": "5Eh1C69jwNgBPtp2oKxJ7Zy5dS6LJn3XZ6CJHPwcAB2iFpZQ",
"amount": 1,
"extended": true
}'
Example response:
{
"error": null,
"result": {
"unsignedTransaction": "0xa8040600070010a5d4e8037427a7dee592c47c6875266357ee7afd763ddfdf6bd1634e410b7335fc38331e",
"unsignedTransactionSerialized": "7b2261646472657373223a22354568314336396a774e6742507470326f4b784a375a79356453364c4a6e33585a36434a485077634142326946705a51222c22626c6f636b48617368223a22307835333039616433373261623530396633363331666664666163633130633066386433396138303339616664666432353265393564653263336636653064306236222c22626c6f636b4e756d626572223a2230783031383734346262222c22657261223a22307862353033222c2267656e6573697348617368223a22307865313433663233383033616335306538663666386536323639356431636539653465316436386161333663316364326366643135333430323133663334323365222c226d65746164617461527063223a223078222c226d6574686f64223a22307830363030303730303130613564346538303337343237613764656535393263343763363837353236363335376565376166643736336464666466366264313633346534313062373333356663333833333165222c226e6f6e6365223a2230783030303030303136222c227369676e6564457874656e73696f6e73223a5b22436865636b4e6f6e5a65726f53656e646572222c22436865636b5370656356657273696f6e222c22436865636b547856657273696f6e222c22436865636b47656e65736973222c22436865636b4d6f7274616c697479222c22436865636b4e6f6e6365222c22436865636b576569676874222c224368617267655472616e73616374696f6e5061796d656e74222c22436865636b4d6574616461746148617368222c225765696768745265636c61696d225d2c227370656356657273696f6e223a2230783030306638383932222c22746970223a2230783030303030303030303030303030303030303030303030303030303030303030222c227472616e73616374696f6e56657273696f6e223a2230783030303030303162222c2276657273696f6e223a347d",
"unsignedTransactionPayload": "0x0600070010a5d4e8037427a7dee592c47c6875266357ee7afd763ddfdf6bd1634e410b7335fc38331e",
"unsignedTransactionObject": {
"blockHash": "0x339985858981806fa80352e0ea533f94e5f92855296aa24b63b00fee7e40a448",
"eraPeriod": 64,
"currentEra": 9186,
"genesisHash": "0xe143f23803ac50e8f6f8e62695d1ce9e4e1d68aa36c1cd2cfd15340213f3423e",
"metadataRpc": "0x",
"method": {
"args": {
"value": "1,000,000,000,000",
"payee": {
"Account": "5Eh1C69jwNgBPtp2oKxJ7Zy5dS6LJn3XZ6CJHPwcAB2iFpZQ"
}
},
"method": "bond",
"section": "staking"
},
"nonce": 22,
"specVersion": 1018002,
"transactionVersion": 27,
"tip": 0
},
"stashAccountAddress": "5Eh1C69jwNgBPtp2oKxJ7Zy5dS6LJn3XZ6CJHPwcAB2iFpZQ",
"rewardDestinationType": "account",
"rewardDestination": "5Eh1C69jwNgBPtp2oKxJ7Zy5dS6LJn3XZ6CJHPwcAB2iFpZQ",
"amount": 1,
"createdAt": "2025-04-17T13:25:14.508Z"
}
}
unsignedTransaction
— unsigned transaction in the hexadecimal format.unsignedTransactionSerialized
— unsigned serialized transaction containing all the data.unsignedTransactionPayload
— only raw payload of the unsigned transaction in the hexadecimal format.unsignedTransactionObject
— full decoded transaction structure with all the fields and metadata:blockHash
— hash of the checkpoint block in which the transaction was included.eraPeriod
— validity period of the transaction, representing the number of blocks following after the checkpoint for which the transaction is valid.currentEra
— current staking era of the transaction.genesisHash
— hash of the genesis block.metadataRpc
— serialized metadata used for offline decoding and transaction signing.method
is the list of data fields containing information on the method called to construct a transaction.nonce
— nonce of the transaction.specVersion
— current version of the chain specification for the runtime.transactionVersion
— current version of the transaction format.tip
— optional fee used to increase the transaction priority.
stashAccountAddress
— main stash account address which keeps tokens for bonding.rewardDestinationType
— rewards destination type:staked
— rewards will be sent to the stash account and added to the current bond (compounding rewards).stash
— rewards will be sent to the stash account as a transferrable balance (not compounding rewards).account
— rewards will be sent to any account specified as a transferrable balance.
rewardDestination
— rewards destination account address.amount
— amount of tokens to bond. DOT is used for the main network, KSM for Kusama, and WND for Westend.createdAt
— timestamp of the transaction in the ISO 8601 format.
Supported endpoints
The extended
parameter is available for the following endpoints:
- /staking/bond
- /staking/nominate
- /staking/pool/bond
- /staking/pool/set-claim-permission
- /staking/pool/claim-payout
- /account/add
Offline transaction signing
To sign the transaction offline, follow these steps:
-
Prepare the
unsignedTransactionSerialized
object. -
Sign the transaction in your preferable way of signing.
Example (for
westend
network):/* Instructions: 1. Install Sidecar and run with the environment variable for Substrate URL: yarn global add @substrate/api-sidecar SAS_SUBSTRATE_URL=wss://westend-ws-proxy.polka.p2p.world substrate-api-sidecar 2. Install dependencies: yarn install 3. Run the script: npx ts-node test.ts */ const API_BASE_URL = 'https://api.p2p.org'; const TOKEN = '*******'; const WS_PROVIDER_URL = 'wss://westend-ws-proxy.polka.p2p.world'; const SECRET_FILE_PATH = 'secret.json'; const TEST_PASSWORD = 'testtest'; const SIDECAR_URL = 'http://localhost:8080/transaction'; const STASH_ACCOUNT_ADDRESS = '5GW6GmWBPhbpWPwRkria91Dn8EjTxEoqVorZxE9gkvTCHVN8'; import { readFileSync } from 'fs'; import { Keyring, WsProvider } from '@polkadot/api'; import { cryptoWaitReady } from '@polkadot/util-crypto'; import { construct, getRegistry, KeyringPair, UnsignedTransaction, } from '@substrate/txwrapper-polkadot'; import { ApiPromise } from '@polkadot/api/cjs/bundle'; interface ApiResponse { result: { unsignedTransactionSerialized: string; }; } interface TransactionResponse { hash: string; } async function getBondTx(data: any): Promise<ApiResponse> { return fetchData( `${API_BASE_URL}/api/v1/polkadot/westend/staking/bond`, data ); } async function getBondExtraTx(data: any): Promise<ApiResponse> { return fetchData( `${API_BASE_URL}/api/v1/polkadot/westend/staking/bond-extra`, data ); } async function fetchData(url: string, data: any): Promise<ApiResponse> { try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Accept: 'application/json', Authorization: 'Bearer ' + TOKEN, }, body: JSON.stringify(data), }); return (await response.json()) as ApiResponse; } catch (error) { console.error('Error fetching data:', error); throw error; } } async function fetchMetadata(): Promise<string> { const provider = new WsProvider(WS_PROVIDER_URL); const api = await ApiPromise.create({ provider }); const metadataRpc = await api.rpc.state.getMetadata(); await api.disconnect(); return metadataRpc.toHex(); } function createKeypair(): KeyringPair { const keyring = new Keyring({ type: 'sr25519' }); const keyInfo = JSON.parse(readFileSync(SECRET_FILE_PATH, 'utf8')); const sender = keyring.addFromJson(keyInfo); sender.decodePkcs8(TEST_PASSWORD); return sender; } function signTransaction(unsignTx: string, metadataRpc: `0x{string}`): string { const keypair = createKeypair(); const registry = getRegistry({ chainName: 'Polkadot', specName: 'westend', specVersion: 1018001, metadataRpc, }); const serialized = unsignTx; const jsonString = Buffer.from(serialized, 'hex').toString('utf-8'); const unsigned = JSON.parse(jsonString); const extrinsicPayload = registry.createType('ExtrinsicPayload', unsigned, { version: unsigned.version, }); const signature = extrinsicPayload.sign(keypair).signature; const rawT = construct.signedTx( unsigned as unknown as UnsignedTransaction, signature, { metadataRpc, registry, userExtensions: { SetEvmOrigin: { payload: {}, extrinsic: {} } }, } ); return rawT; } async function sendTransaction( transactionData: string ): Promise<TransactionResponse> { try { const response = await fetch(SIDECAR_URL, { method: 'POST', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ tx: transactionData }), }); return (await response.json()) as TransactionResponse; } catch (error) { console.error('Error sending transaction:', error); throw error; } } void (async () => { await cryptoWaitReady(); // Get Bond Transaction const unsignTx = await getBondTx({ stashAccountAddress: STASH_ACCOUNT_ADDRESS, rewardDestinationType: 'account', rewardDestination: STASH_ACCOUNT_ADDRESS, amount: 1, extended: true, }); /* // Get Bond Extra Transaction const unsignTx = await getBondExtraTx({ stashAccountAddress: STASH_ACCOUNT_ADDRESS, amount: 0.001, extended: true, }); */ console.log('unsignTx', unsignTx); // Fetch Metadata const metadataRpc = await fetchMetadata(); // Sign Transaction const signTx = signTransaction( unsignTx.result.unsignedTransactionSerialized, metadataRpc as `0x{string}` ); console.log('signTx', signTx); // Send Signed Transaction const result = await sendTransaction(signTx); console.log('result', result); })();
What's Next?
- Getting Started.
- Withdrawal.
- Staking API reference.
Updated 2 days ago