0

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.

1 Answer 1

0

The problem was that I was not generating the payload correctly. I solved this by using createType where possible for generating the payload types E.G.

    const latestBlockHash = (await api.rpc.chain.getFinalizedHead()).toHex();
    const blockNumber = (await api.rpc.chain.getBlock(latestBlockHash)).block.header.number.toHex();
    const era = api.registry.createType('ExtrinsicEra', {
      current: blockNumber,
      period: 64
    });

    const nonceRaw = await api.call.accountNonceApi.accountNonce(from);
    const nonce = api.registry.createType('Compact<Index>', nonceRaw.toNumber());

    const payload = {
      address: from,
      blockHash: latestBlockHash,
      blockNumber,
      era: era.toHex(),
      genesisHash: api.genesisHash.toHex(),
      method: tx.method.toHex(),
      nonce: nonce.toHex(),
      signedExtensions: api.registry.signedExtensions,
      specVersion: api.runtimeVersion.specVersion.toHex(),
      tip: api.registry.createType('Compact<Balance>', 0).toHex(),
      transactionVersion: api.runtimeVersion.transactionVersion.toHex(),
      version: tx.version
    };

    const extrinsicPayload = api.registry.createType('ExtrinsicPayload', payload, {
      version: payload.version
    });

Then the signing generated a valid signature.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.