Skip to content

Commit e605d7c

Browse files
committed
try to fix era and nonce
1 parent 053e7ce commit e605d7c

File tree

4 files changed

+675
-506
lines changed

4 files changed

+675
-506
lines changed

tests_integration/index.ts

Lines changed: 164 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import transport from "@ledgerhq/hw-transport-node-hid"
44
import { hexToU8a } from '@polkadot/util';
55
import { blake2AsHex } from '@polkadot/util-crypto';
66
import axios from "axios";
7-
import {ExtrinsicPayloadValue} from "@polkadot/types/types/extrinsic";
7+
import { ExtrinsicPayloadValue } from "@polkadot/types/types/extrinsic";
88

99
// Function to initialize Ledger
1010
async function initLedger() {
@@ -16,6 +16,64 @@ async function initLedger() {
1616
return ledger;
1717
}
1818

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+
1977
// Reference: https://github.com/polkadot-js/api/issues/1421
2078

2179
// ⚠️ WARNING: This test uses POLKADOT MAINNET with REAL DOT tokens that have real monetary value!
@@ -27,15 +85,19 @@ async function main() {
2785
const wsProvider = new WsProvider('wss://rpc.polkadot.io');
2886
const api = await ApiPromise.create({ provider: wsProvider });
2987

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+
3093
// Initialize Ledger
3194
const ledger = await initLedger();
3295

33-
// Define sender and receiver addresses and the amount to transfer
96+
// Get sender address
3497
const senderAddress = await ledger.getAddress("m/44'/354'/0'/0'/0'", 0);
98+
console.log("\nSender address: " + senderAddress.address);
3599

36-
console.log("sender address " + senderAddress.address)
37-
38-
// Get account information including balance and nonce
100+
// Get account information including balance
39101
const accountInfo = await api.query.system.account(senderAddress.address);
40102
const accountData = accountInfo.toHuman() as any;
41103

@@ -46,74 +108,139 @@ async function main() {
46108
console.log(" Reserved balance: " + balance.reserved);
47109
console.log(" Frozen balance: " + balance.frozen);
48110

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);
52116

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);
55120

56121
// Display the encoded call (method) information
57122
const encodedCall = remark.method.toHex();
58123
const encodedCallHash = blake2AsHex(encodedCall, 256);
59124

60-
console.log("\nEncoded Call Information:");
125+
console.log("\nTransaction Information:");
126+
console.log(" Call: system.remark");
127+
console.log(" Remark data: " + remarkData);
61128
console.log(" Encoded call data: " + encodedCall);
62129
console.log(" Encoded call hash: " + encodedCallHash);
63130
console.log(" Method human: " + JSON.stringify(remark.method.toHuman()));
64131

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);
66135

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)
70137
const payload = api.createType('ExtrinsicPayload', {
71138
method: remark.method.toHex(),
72-
nonce: nonce as unknown as number,
139+
nonce: nonce,
73140
genesisHash: api.genesisHash,
74-
blockHash: api.genesisHash,
141+
blockHash: blockHash, // Use checkpoint block hash, not genesis
75142
transactionVersion: api.runtimeVersion.transactionVersion,
76143
specVersion: api.runtimeVersion.specVersion,
77144
runtimeVersion: api.runtimeVersion,
78145
version: api.extrinsicVersion,
146+
era: era, // Use proper era instead of default
147+
tip: 0,
79148
mode: 1,
80-
metadataHash: hexToU8a("01" + resp.data.metadataHash)
149+
metadataHash: hexToU8a("0x01" + resp.data.metadataHash) // Use "01" prefix for 32 bytes
81150
});
82151

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)));
85155

86156
// 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"));
89160

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
94164
genesisHash: api.genesisHash,
95-
blockHash: api.genesisHash,
165+
blockHash: blockHash, // Use the same block hash
96166
method: remark.method.toHex(),
97-
nonce: nonce as unknown as number,
167+
nonce: nonce,
98168
specVersion: api.runtimeVersion.specVersion,
99169
tip: 0,
100170
transactionVersion: api.runtimeVersion.transactionVersion,
101171
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)));
104191

105192
// 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);
107217

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)));
110221

111222
// 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+
}
114240
});
115241

116-
await new Promise((resolve ) => setTimeout(resolve, 120000))
242+
// Wait for transaction completion or timeout
243+
await new Promise((resolve) => setTimeout(resolve, 120000));
117244
}
118245

119-
main().catch(console.error).finally(() => process.exit());
246+
main().catch(console.error).finally(() => process.exit());

tests_integration/package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
"start": "ts-node index.ts"
88
},
99
"dependencies": {
10-
"@ledgerhq/hw-transport-node-hid": "^6.28.6",
11-
"@polkadot/api": "^11.2.1",
12-
"@polkadot/util": "^12.6.2",
10+
"@ledgerhq/hw-transport-node-hid": "^6.29.10",
11+
"@polkadot/api": "^16.4.6",
12+
"@polkadot/util": "^13.5.6",
1313
"@polkadot/util-crypto": "^13.5.6",
14-
"@zondax/ledger-substrate": "^0.44.2",
15-
"axios": "^1.7.2",
14+
"@zondax/ledger-substrate": "^1.1.2",
15+
"axios": "^1.11.0",
1616
"ts-node": "^10.9.2",
17-
"typescript": "^5.4.5"
17+
"typescript": "^5.9.2"
1818
}
1919
}

tests_integration/tsconfig.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"compilerOptions": {
3+
"target": "es2020",
4+
"module": "commonjs",
5+
"lib": ["es2020"],
6+
"allowJs": true,
7+
"outDir": "./dist",
8+
"rootDir": "./",
9+
"strict": true,
10+
"moduleResolution": "node",
11+
"esModuleInterop": true,
12+
"skipLibCheck": true,
13+
"forceConsistentCasingInFileNames": true,
14+
"resolveJsonModule": true
15+
},
16+
"ts-node": {
17+
"esm": false
18+
}
19+
}

0 commit comments

Comments
 (0)