I would like to understand how to sign a balances.transferKeepAlive transaction completely offline with an existing signing service with ECDSA signatures. This is what I have so far that is working:
const { ApiPromise, WsProvider } = require('@polkadot/api');
const { EXTRINSIC_VERSION } = require('@polkadot/types/extrinsic/v4/Extrinsic');
const { cryptoWaitReady, keccakAsHex } = require('@polkadot/util-crypto');
const { secp256k1 } = require('@noble/curves/secp256k1');
const crypto = require('node:crypto')
const { hexToU8a,u8aConcat, u8aToHex } = require('@polkadot/util');
class SigningService {
constructor() {
this.keys = {}
}
computePublicKeysFromPrivateKey({ key }) {
return Promise.resolve({
publicKeys: [
{ format: 'compressed', value: Buffer.from(secp256k1.getPublicKey(key, true)).toString('hex') },
{ format: 'uncompressed', value: Buffer.from(secp256k1.getPublicKey(key, false)).toString('hex').substring(2) }
]
});
}
async importPrivateKey({ key }) {
const id = crypto.randomUUID();
const publicKeys = await this.computePublicKeysFromPrivateKey({ key });
this.keys[id] = key;
return Promise.resolve({ id, ...publicKeys });
}
async sign({ hash, id }) {
const signature = secp256k1.sign(hash, keys[id]);
return Promise.resolve({ recovery: signature.recovery, signature: signature.toCompactHex(), signatureFull: signature });
}
}
const transferECDSA = async (to, from, amount) => {
const keyService = new SigningService();
const { id: keyId } = await keyService.importPrivateKey({ key: '<private-key-hex-for-from>' });
const RPC_ENDPOINT = 'wss://rococo-muse-rpc.polkadot.io';
const wsProvider = new WsProvider(RPC_ENDPOINT);
const api = await ApiPromise.create({ provider: wsProvider });
await api.isReady;
await cryptoWaitReady();
const extrinsic = api.tx.balances.transferKeepAlive(to, amount);
const signer = {
signPayload: async payload => {
const extrinsicPayload = api.registry.createType('ExtrinsicPayload', payload, { mode: 0, registry: api.registry, version: EXTRINSIC_VERSION });
const extrinsicPayloadU8a = extrinsicPayload.toU8a({ method: true });
const actualPayload = extrinsicPayloadU8a.length > 256 ? api.registry.hash(extrinsicPayloadU8a) : extrinsicPayloadU8a;
const { recovery, signature } = await keyService.sign({ hash: keccakAsHex(actualPayload).substring(2), id: keyId });
const recoveryU8a = Uint8Array.from([recovery || 0]);
return { id: 1, signature: u8aToHex(u8aConcat(hexToU8a(signature), recoveryU8a)) };
}
};
const signedExtrinsic = await extrinsic.signAsync(from, { signer });
const unsub = await signedExtrinsic.send(({ status }) => {
if (status.isInBlock) {
console.log(`Transaction included at blockHash ${status.asInBlock}`);
} else if (status.isFinalized) {
console.log(`Transaction finalized at blockHash ${status.asFinalized}`);
unsub();
throw new Error('Exiting Process');
}
});
}
My goal is to be able to create the signer and sign completely offline. Currently the const extrinsicPayload = api.registry.createType('ExtrinsicPayload', payload, { mode: 0, registry: api.registry, version: EXTRINSIC_VERSION });
bit in the signer is keeping me from having the signer completely offline. I have tried countless other approaches and I cannot generate a valid signature for some reason. Any help is greatly appreciated.