@@ -4,7 +4,7 @@ import transport from "@ledgerhq/hw-transport-node-hid"
4
4
import { hexToU8a } from '@polkadot/util' ;
5
5
import { blake2AsHex } from '@polkadot/util-crypto' ;
6
6
import axios from "axios" ;
7
- import { ExtrinsicPayloadValue } from "@polkadot/types/types/extrinsic" ;
7
+ import { ExtrinsicPayloadValue } from "@polkadot/types/types/extrinsic" ;
8
8
9
9
// Function to initialize Ledger
10
10
async function initLedger ( ) {
@@ -16,6 +16,64 @@ async function initLedger() {
16
16
return ledger ;
17
17
}
18
18
19
+ // Helper function to get proper era information
20
+ async function getEraInfo ( api : ApiPromise , mortal : boolean = true , eraPeriod : number = 64 ) {
21
+ if ( ! mortal ) {
22
+ // Immortal transaction - not recommended but supported
23
+ return {
24
+ era : api . createType ( 'ExtrinsicEra' , '0x00' ) ,
25
+ blockHash : api . genesisHash ,
26
+ blockNumber : 0
27
+ } ;
28
+ }
29
+
30
+ // For mortal transactions, get current block info
31
+ const currentBlock = await api . rpc . chain . getHeader ( ) ;
32
+ const currentBlockNumber = currentBlock . number . toNumber ( ) ;
33
+
34
+ // Calculate checkpoint block (should be recent but not the latest for stability)
35
+ const checkpointBlockNumber = Math . max ( 0 , currentBlockNumber - 10 ) ;
36
+ const checkpointBlockHash = await api . rpc . chain . getBlockHash ( checkpointBlockNumber ) ;
37
+
38
+ // Create mortal era with specified period
39
+ const era = api . createType ( 'ExtrinsicEra' , {
40
+ current : currentBlockNumber ,
41
+ period : eraPeriod
42
+ } ) ;
43
+
44
+ console . log ( `\nEra Information:` ) ;
45
+ console . log ( ` Mortal: true` ) ;
46
+ console . log ( ` Current block: ${ currentBlockNumber } ` ) ;
47
+ console . log ( ` Checkpoint block: ${ checkpointBlockNumber } ` ) ;
48
+ console . log ( ` Era period: ${ eraPeriod } blocks` ) ;
49
+ console . log ( ` Transaction valid until block: ${ currentBlockNumber + eraPeriod } ` ) ;
50
+ console . log ( ` Checkpoint block hash: ${ checkpointBlockHash . toHex ( ) } ` ) ;
51
+
52
+ return {
53
+ era,
54
+ blockHash : checkpointBlockHash ,
55
+ blockNumber : checkpointBlockNumber
56
+ } ;
57
+ }
58
+
59
+ // Helper function to get the next nonce safely
60
+ async function getNextNonce ( api : ApiPromise , address : string ) {
61
+ // Get both current nonce and next index to handle pending transactions
62
+ const accountInfo = await api . query . system . account ( address ) ;
63
+ const currentNonce = ( accountInfo as any ) . nonce . toNumber ( ) ;
64
+ const nextIndex = ( await api . rpc . system . accountNextIndex ( address ) ) . toNumber ( ) ;
65
+
66
+ // Use the higher value to account for pending transactions
67
+ const nonce = Math . max ( currentNonce , nextIndex ) ;
68
+
69
+ console . log ( `\nNonce Information:` ) ;
70
+ console . log ( ` Current account nonce: ${ currentNonce } ` ) ;
71
+ console . log ( ` Next index from RPC: ${ nextIndex } ` ) ;
72
+ console . log ( ` Using nonce: ${ nonce } ` ) ;
73
+
74
+ return nonce ;
75
+ }
76
+
19
77
// Reference: https://github.com/polkadot-js/api/issues/1421
20
78
21
79
// ⚠️ WARNING: This test uses POLKADOT MAINNET with REAL DOT tokens that have real monetary value!
@@ -27,15 +85,19 @@ async function main() {
27
85
const wsProvider = new WsProvider ( 'wss://rpc.polkadot.io' ) ;
28
86
const api = await ApiPromise . create ( { provider : wsProvider } ) ;
29
87
88
+ console . log ( `Connected to Polkadot network:` ) ;
89
+ console . log ( ` Chain: ${ await api . rpc . system . chain ( ) } ` ) ;
90
+ console . log ( ` Runtime version: ${ api . runtimeVersion . specVersion . toNumber ( ) } ` ) ;
91
+ console . log ( ` Transaction version: ${ api . runtimeVersion . transactionVersion . toNumber ( ) } ` ) ;
92
+
30
93
// Initialize Ledger
31
94
const ledger = await initLedger ( ) ;
32
95
33
- // Define sender and receiver addresses and the amount to transfer
96
+ // Get sender address
34
97
const senderAddress = await ledger . getAddress ( "m/44'/354'/0'/0'/0'" , 0 ) ;
98
+ console . log ( "\nSender address: " + senderAddress . address ) ;
35
99
36
- console . log ( "sender address " + senderAddress . address )
37
-
38
- // Get account information including balance and nonce
100
+ // Get account information including balance
39
101
const accountInfo = await api . query . system . account ( senderAddress . address ) ;
40
102
const accountData = accountInfo . toHuman ( ) as any ;
41
103
@@ -46,74 +108,139 @@ async function main() {
46
108
console . log ( " Reserved balance: " + balance . reserved ) ;
47
109
console . log ( " Frozen balance: " + balance . frozen ) ;
48
110
49
- // Extract nonce
50
- const { nonce} = accountData ;
51
- console . log ( "\nTransaction nonce: " + nonce )
111
+ // Get proper nonce
112
+ const nonce = await getNextNonce ( api , senderAddress . address ) ;
113
+
114
+ // Get proper era information (mortal transaction with 64 block period)
115
+ const { era, blockHash, blockNumber } = await getEraInfo ( api , true , 64 ) ;
52
116
53
- // Create the transfer transaction
54
- const remark = api . tx . system . remark ( "0x000000000000" ) ;
117
+ // Create the remark transaction
118
+ const remarkData = "0x00000000000000" ;
119
+ const remark = api . tx . system . remark ( remarkData ) ;
55
120
56
121
// Display the encoded call (method) information
57
122
const encodedCall = remark . method . toHex ( ) ;
58
123
const encodedCallHash = blake2AsHex ( encodedCall , 256 ) ;
59
124
60
- console . log ( "\nEncoded Call Information:" ) ;
125
+ console . log ( "\nTransaction Information:" ) ;
126
+ console . log ( " Call: system.remark" ) ;
127
+ console . log ( " Remark data: " + remarkData ) ;
61
128
console . log ( " Encoded call data: " + encodedCall ) ;
62
129
console . log ( " Encoded call hash: " + encodedCallHash ) ;
63
130
console . log ( " Method human: " + JSON . stringify ( remark . method . toHuman ( ) ) ) ;
64
131
65
- const resp = await axios . post ( "https://api.zondax.ch/polkadot/node/metadata/hash" , { id : 'dot' } )
132
+ // Get metadata hash for CheckMetadataHash mode
133
+ const resp = await axios . post ( "https://api.zondax.ch/polkadot/node/metadata/hash" , { id : 'dot' } ) ;
134
+ console . log ( "\nMetadata hash from Zondax: " + resp . data . metadataHash ) ;
66
135
67
- console . log ( "metadata hash " + resp . data . metadataHash )
68
-
69
- // Create the payload for signing
136
+ // Create the payload for signing with proper era and block hash (WITH CheckMetadataHash)
70
137
const payload = api . createType ( 'ExtrinsicPayload' , {
71
138
method : remark . method . toHex ( ) ,
72
- nonce : nonce as unknown as number ,
139
+ nonce : nonce ,
73
140
genesisHash : api . genesisHash ,
74
- blockHash : api . genesisHash ,
141
+ blockHash : blockHash , // Use checkpoint block hash, not genesis
75
142
transactionVersion : api . runtimeVersion . transactionVersion ,
76
143
specVersion : api . runtimeVersion . specVersion ,
77
144
runtimeVersion : api . runtimeVersion ,
78
145
version : api . extrinsicVersion ,
146
+ era : era , // Use proper era instead of default
147
+ tip : 0 ,
79
148
mode : 1 ,
80
- metadataHash : hexToU8a ( "01 " + resp . data . metadataHash )
149
+ metadataHash : hexToU8a ( "0x01 " + resp . data . metadataHash ) // Use "01" prefix for 32 bytes
81
150
} ) ;
82
151
83
- console . log ( "payload to sign[hex] " + Buffer . from ( payload . toU8a ( true ) ) . toString ( "hex" ) )
84
- console . log ( "payload to sign[human] " + JSON . stringify ( payload . toHuman ( true ) ) )
152
+ console . log ( "\nPayload Information:" ) ;
153
+ console . log ( " Payload to sign [hex]: " + Buffer . from ( payload . toU8a ( true ) ) . toString ( "hex" ) ) ;
154
+ console . log ( " Payload to sign [human]: " + JSON . stringify ( payload . toHuman ( true ) ) ) ;
85
155
86
156
// Request signature from Ledger
87
- // Remove first byte as it indicates the length, and it is not supported by shortener and ledger app
88
- const { signature } = await ledger . sign ( "m/44'/354'/0'/0'/0'" , Buffer . from ( payload . toU8a ( true ) ) ) ;
157
+ console . log ( "\nSigning with Ledger..." ) ;
158
+ const { signature } = await ledger . signEd25519 ( "m/44'/354'/0'/0'/0'" , Buffer . from ( payload . toU8a ( true ) ) ) ;
159
+ console . log ( "Signature: " + signature . toString ( "hex" ) ) ;
89
160
90
- console . log ( "signature " + signature . toString ( "hex" ) )
91
-
92
- const payloadValue :ExtrinsicPayloadValue = {
93
- era : payload . era ,
161
+ // Create payload value for addSignature (must match the signed payload EXACTLY)
162
+ const payloadValue : ExtrinsicPayloadValue = {
163
+ era : era , // Use the same era
94
164
genesisHash : api . genesisHash ,
95
- blockHash : api . genesisHash ,
165
+ blockHash : blockHash , // Use the same block hash
96
166
method : remark . method . toHex ( ) ,
97
- nonce : nonce as unknown as number ,
167
+ nonce : nonce ,
98
168
specVersion : api . runtimeVersion . specVersion ,
99
169
tip : 0 ,
100
170
transactionVersion : api . runtimeVersion . transactionVersion ,
101
171
mode : 1 ,
102
- metadataHash : hexToU8a ( "01" + resp . data . metadataHash )
103
- }
172
+ metadataHash : hexToU8a ( "0x01" + resp . data . metadataHash ) // Match the payload
173
+ } ;
174
+
175
+ // Debug: Compare what we signed vs what we're including
176
+ console . log ( "\n=== SIGNATURE VERIFICATION DEBUG ===" ) ;
177
+ console . log ( "Original payload era:" , JSON . stringify ( ( payload as any ) . era . toHuman ( ) ) ) ;
178
+ console . log ( "PayloadValue era:" , JSON . stringify ( era . toHuman ( ) ) ) ;
179
+ console . log ( "Original payload blockHash:" , ( payload as any ) . blockHash . toHex ( ) ) ;
180
+ console . log ( "PayloadValue blockHash:" , blockHash . toHex ( ) ) ;
181
+ console . log ( "Original payload nonce:" , ( payload as any ) . nonce . toNumber ( ) ) ;
182
+ console . log ( "PayloadValue nonce:" , nonce ) ;
183
+ console . log ( "Original payload specVersion:" , ( payload as any ) . specVersion . toNumber ( ) ) ;
184
+ console . log ( "PayloadValue specVersion:" , api . runtimeVersion . specVersion . toNumber ( ) ) ;
185
+
186
+ // Recreate the exact same payload for verification
187
+ const verificationPayload = api . createType ( 'ExtrinsicPayload' , payloadValue ) ;
188
+ console . log ( "Verification payload hex:" , verificationPayload . toU8a ( true ) . toString ( ) ) ;
189
+ console . log ( "Original payload hex: " , payload . toU8a ( true ) . toString ( ) ) ;
190
+ console . log ( "Payloads match:" , JSON . stringify ( verificationPayload . toU8a ( true ) ) === JSON . stringify ( payload . toU8a ( true ) ) ) ;
104
191
105
192
// Combine the payload and signature to create a signed extrinsic
106
- const signedExtrinsic = remark . addSignature ( senderAddress . address , signature , payloadValue ) ;
193
+ // The Ledger returns signature in a specific format - check first byte for signature type
194
+ console . log ( `Raw signature length: ${ signature . length } bytes` ) ;
195
+ console . log ( `First byte of signature: 0x${ signature [ 0 ] . toString ( 16 ) . padStart ( 2 , '0' ) } ` ) ;
196
+
197
+ let cleanSignature : Buffer ;
198
+ if ( signature . length === 65 && signature [ 0 ] === 0x00 ) {
199
+ // Ed25519 signature with type prefix
200
+ cleanSignature = signature . slice ( 1 ) ;
201
+ } else if ( signature . length === 64 ) {
202
+ // Already correct length for Ed25519
203
+ cleanSignature = signature ;
204
+ } else {
205
+ throw new Error ( `Unexpected signature format: ${ signature . length } bytes` ) ;
206
+ }
207
+
208
+ console . log ( `Clean signature length: ${ cleanSignature . length } bytes` ) ;
209
+
210
+ // Create MultiSignature explicitly for Ed25519
211
+ const multiSig = api . createType ( 'MultiSignature' , {
212
+ Ed25519 : `0x${ cleanSignature . toString ( "hex" ) } `
213
+ } ) ;
214
+
215
+ console . log ( `MultiSignature: ${ multiSig . toHex ( ) } ` ) ;
216
+ const signedExtrinsic = remark . addSignature ( senderAddress . address , multiSig . toHex ( ) , payloadValue ) ;
107
217
108
- console . log ( "signedTx to broadcast[hex] " + Buffer . from ( signedExtrinsic . toU8a ( ) ) . toString ( "hex" ) )
109
- console . log ( "signedTx to broadcast[human] " + JSON . stringify ( signedExtrinsic . toHuman ( true ) ) )
218
+ console . log ( "\nSigned Transaction Information:" ) ;
219
+ console . log ( " Signed tx [hex]: " + Buffer . from ( signedExtrinsic . toU8a ( ) ) . toString ( "hex" ) ) ;
220
+ console . log ( " Signed tx [human]: " + JSON . stringify ( signedExtrinsic . toHuman ( true ) ) ) ;
110
221
111
222
// Submit the signed transaction
112
- await remark . send ( ( status ) => {
113
- console . log ( `Tx status: ${ JSON . stringify ( status ) } ` ) ;
223
+ console . log ( "\nSubmitting transaction..." ) ;
224
+ const unsub = await api . rpc . author . submitAndWatchExtrinsic ( signedExtrinsic , ( status ) => {
225
+ console . log ( `Transaction status: ${ status . type } ` ) ;
226
+
227
+ if ( status . isInBlock ) {
228
+ console . log ( `Transaction included in block: ${ status . asInBlock . toHex ( ) } ` ) ;
229
+ }
230
+
231
+ if ( status . isFinalized ) {
232
+ console . log ( `Transaction finalized in block: ${ status . asFinalized . toHex ( ) } ` ) ;
233
+ unsub ( ) ;
234
+ }
235
+
236
+ if ( status . isInvalid || status . isDropped || status . isUsurped ) {
237
+ console . log ( `Transaction failed: ${ status . type } ` ) ;
238
+ unsub ( ) ;
239
+ }
114
240
} ) ;
115
241
116
- await new Promise ( ( resolve ) => setTimeout ( resolve , 120000 ) )
242
+ // Wait for transaction completion or timeout
243
+ await new Promise ( ( resolve ) => setTimeout ( resolve , 120000 ) ) ;
117
244
}
118
245
119
- main ( ) . catch ( console . error ) . finally ( ( ) => process . exit ( ) ) ;
246
+ main ( ) . catch ( console . error ) . finally ( ( ) => process . exit ( ) ) ;
0 commit comments