XRPLF / xrpl.js

A JavaScript/TypeScript API for interacting with the XRP Ledger in Node.js and the browser
https://xrpl.org/
1.2k stars 511 forks source link

How to sign a transaction using private key? #1941

Closed YakovL closed 1 year ago

YakovL commented 2 years ago

As far as I understand, this should be possible. On the other hand, basic example provided by docs uses Wallet which can't be created from private key (as far as I understand; at least it only has methods fromSeed, fromSecret, fromMnemonic and fromEntropy).

I've found the sign method in another library, ripple-keypairs (searching by "sign" in this repo), but await client.autofill(transaction) returns xrpl.Payment and sign expects hex as the first argument.

Can anyone help with getting a signed tx via either ripple-keypairs's sign (how to xrpl.Payment to hex?) or via xrpl itself?

mvadari commented 2 years ago

You can use sign in ripple-keypairs with a private key if you encode the transaction using encodeForSigning in ripple-binary-codec.

YakovL commented 2 years ago

Thanks! Can I somehow use the result with client.submitAndWait?

Here's what I'm trying to do:

import { sign } from "ripple-keypairs";
import { encodeForSigning } from "ripple-binary-codec";
...

await client.connect();
const transaction: Transaction = {
    TransactionType: "Payment",
    Account: address,
    Amount: amount,
    Destination: recipientAddress,
};
const preparedTransaction = await client.autofill(transaction);
const preparedTransactionHex = encodeForSigning(preparedTransaction);
const signedTransaction = sign(preparedTransactionHex, privateKey);
const tentativeResultTx = await client.submitAndWait(signedTransaction); // this is ripple.ts:204:52

but for the last line I'm getting

TypeError: Cannot read properties of undefined (reading 'name') 
  at Function.STObject.fromParser (<project folder>\node_modules\ripple-binary-codec\src\types\st-object.ts:66:17)
  at BinaryParser.readType (<project folder>\node_modules\ripple-binary-codec\src\serdes\binary-parser.ts:159:17)
  at readJSON (<project folder>\node_modules\ripple-binary-codec\src\binary.ts:30:11)
  at binaryToJSON (<project folder>\node_modules\ripple-binary-codec\src\binary.ts:38:53)
  at decode (<project folder>\node_modules\ripple-binary-codec\src\index.ts:24:10)
  at isSigned (<project folder>\node_modules\xrpl\src\sugar\submit.ts:164:54)
  at <project folder>\node_modules\xrpl\src\sugar\submit.ts:187:7
  at Generator.next (<anonymous>)
  at <project folder>\node_modules\xrpl\dist\npm\sugar\submit.js:8:71
  at new Promise (<anonymous>)
  at __awaiter (<project folder>\node_modules\xrpl\dist\npm\sugar\submit.js:4:12)
  at getSignedTx (<project folder>\node_modules\xrpl\dist\npm\sugar\submit.js:94:12)
  at Client.<anonymous> (<project folder>\node_modules\xrpl\src\sugar\submit.ts:75:26)
  at Generator.next (<anonymous>)
  at <project folder>\node_modules\xrpl\dist\npm\sugar\submit.js:8:71
  at new Promise (<anonymous>)
  at __awaiter (<project folder>\node_modules\xrpl\dist\npm\sugar\submit.js:4:12)
  at Client.submitAndWait (<project folder>\node_modules\xrpl\dist\npm\sugar\submit.js:32:12)
  at Ripple.sendNativeToken (<project folder>\src\implementations\ripple.ts:204:52)
  at processTicksAndRejections (node:internal/process/task_queues:96:5)
  at Context.<anonymous> (<project folder>\test\ripple.test.ts:555:24)

Any ideas what can be wrong?

console.log(signedTransaction) gives 304402202124E9F4F2B5A30904E1C10863BB2E55D78A7E4422931F952F3A5D177C3819F002202C4F3CA62FD5B276BFDF378907BB5D029CAD0822315EDDBD3E5816684BDFD140. Not sure why the reading 'name' error, submitAndWait allows string argument; presumably this is something internal.

mvadari commented 2 years ago

sign just generates the signature. You need to submit the whole transaction blob, because otherwise rippled doesn't know what the transaction is (you can't really get the transaction details from the signature).

What you want is something like this:

const preparedTransaction = await client.autofill(transaction);
const preparedTransactionHex = encodeForSigning(preparedTransaction);
const signature = sign(preparedTransactionHex, privateKey);
preparedTransaction["TxnSignature"] = signature
preparedTransaction["SigningPublicKey"] = publicKey;
const tentativeResultTx = await client.submitAndWait(preparedTransaction)
YakovL commented 2 years ago

Thanks! This gives an idea of what this can look like; however, the signature doesn't seem to be correct. When I try

await client.connect();
const transaction: Transaction = {
    TransactionType: "Payment",
    Account: address,
    Amount: amount, // ~integer in "drops" of XRP, where 1,000,000 drops equals 1 XRP
    Destination: to,
};
const preparedTransaction = await client.autofill(transaction);

const preparedTransactionHex = encodeForSigning(preparedTransaction);
const signature = sign(preparedTransactionHex, privateKey);
preparedTransaction.TxnSignature = signature;
preparedTransaction.SigningPubKey = publicKey;
const tentativeResultTx = await client.submitAndWait(preparedTransaction);

I'm getting RippledError: invalidTransaction with error_exception: 'fails local checks: Invalid signature.'

Since there's no detail, I'm not really sure what's the source of the problem/how to debug.

Because I'm trying this in testnet with a test account, I can share the credentials to repduce this:

address: rDKTggHSR3ovNNYMk1bxCR5Z7hE9Szmos6 publicKey: 036F9888A9C4E30AD59053D6640BCA5FF54809B146E19BA272EEC06D6913406053 privateKey: 00D8DC02C0E95CD83E720180EEAFA2A3FD580A762C17010525CC3650008A4AB000

Any ideas what could have gone wrong?

mvadari commented 2 years ago

You need to include the SigningPubKey in the signature when signing.

await client.connect();
const transaction: Transaction = {
    TransactionType: "Payment",
    Account: address,
    Amount: amount, // ~integer in "drops" of XRP, where 1,000,000 drops equals 1 XRP
    Destination: to,
};
const preparedTransaction = await client.autofill(transaction);

preparedTransaction.SigningPubKey = publicKey; // HERE: move this up above the encoding
const preparedTransactionHex = encodeForSigning(preparedTransaction);
const signature = sign(preparedTransactionHex, privateKey);
preparedTransaction.TxnSignature = signature; 
const tentativeResultTx = await client.submitAndWait(preparedTransaction);