MPC transaction "Pending Signature"

Hi, I’m testing NCW things in the sandbox environment with below settings.

NCW Web Demo
NCW Backend Demo : using example .env with my api key information, local db

With above setup, in the Web, generating device ID, wallet ID, MPC key works well and can be found in my local DB.

But the transaction has stuck in the “pending signature” status.

Then I add webhook configure using ngrok, running with api co-signer example code, but nothing changed.

Here is the whole logs of the web demo

{"level":"INFO","message":"API call completed","data":{"method":"get_cloud_cosigner_certificate"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T06:55:05.308Z","id":493} {"level":"INFO","message":"Calling API","data":{"method":"get_cloud_cosigner_certificate"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T06:55:03.674Z","id":492} {"level":"INFO","message":"using cached service certificates","deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T06:55:03.673Z","id":491} {"level":"INFO","message":"MPC setup completed","data":{"algorithm":"MPC_CMP_ECDSA_SECP256K1"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:59.136Z","id":490} {"level":"INFO","message":"API call completed","data":{"method":"send_mpc_public_keys"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:59.129Z","id":489} {"level":"INFO","message":"Calling API","data":{"method":"send_mpc_public_keys"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:58.612Z","id":488} {"level":"INFO","message":"API call completed","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:58.539Z","id":487} {"level":"INFO","message":"Calling API","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:57.910Z","id":486} {"level":"INFO","message":"API call completed","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:56.564Z","id":485} {"level":"INFO","message":"Calling API","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:54.700Z","id":484} {"level":"INFO","message":"API call completed","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:53.143Z","id":483} {"level":"INFO","message":"Calling API","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:52.467Z","id":482} {"level":"INFO","message":"API call completed","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:52.425Z","id":481} {"level":"INFO","message":"Calling API","data":{"method":"broadcast_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:51.664Z","id":480} {"level":"INFO","message":"API call completed","data":{"method":"poll_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:51.094Z","id":479} {"level":"INFO","message":"Calling API","data":{"method":"poll_mpc_msg"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:50.478Z","id":478} {"level":"INFO","message":"API call completed","data":{"method":"request_mpc_setup"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:50.476Z","id":477} {"level":"INFO","message":"Calling API","data":{"method":"request_mpc_setup"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:49.839Z","id":476} {"level":"INFO","message":"API call completed","data":{"method":"enroll_player"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:49.825Z","id":475} {"level":"INFO","message":"Calling API","data":{"method":"enroll_player"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:48.641Z","id":474} {"level":"INFO","message":"API call completed","data":{"method":"get_all_keys"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:48.613Z","id":473} {"level":"INFO","message":"Calling API","data":{"method":"get_all_keys"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:47.401Z","id":472} {"level":"INFO","message":"API call completed","data":{"method":"get_cloud_cosigner_certificate"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:05.752Z","id":471} {"level":"INFO","message":"Calling API","data":{"method":"get_cloud_cosigner_certificate"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:05.054Z","id":470} {"level":"INFO","message":"using downloaded service certificates","deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:05.054Z","id":469} {"level":"INFO","message":"API call completed","data":{"method":"get_service_certificates"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:05.053Z","id":468} {"level":"INFO","message":"Calling API","data":{"method":"get_service_certificates"},"deviceId":"b5ecf427-fb85-4b7b-adbb-3678e1bcda20","timestamp":"2024-03-12T02:22:03.407Z","id":467}

The docs said the co-signer do auto-sign but it doesn’t in my case.
What should I do to sign the transaction?

Hi @hoodie!

If your transaction is stuck in Pending Signature, then it means it was created successfully and is going to your “designated signer” , Can you confirm if your cosigner is fully paired and running, it should be automatically signing these transactions.

@hoodie
I want to confirm if webhooks are being delivered to the backend at all, can you send your webhook URL so I can check logs on our side?

Hi @Mohammed
This is my webhook URL https://mint-nearly-molly.ngrok-free.app
And I don’t know how to confirm my cosigner is fully paired.
Can you give me some more information about the cosigner setup?

edit) I’m using below example code from the document with my new private-publickey pair (not my NCW_SIGNER key pair)

/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require("fs");
const express = require("express");
const jwt = require("jsonwebtoken");
const bodyParser = require("body-parser");
const { FeeMarketEIP1559Transaction } = require('@ethereumjs/tx');

// Read the callback handler private key
const privateKey = fs.readFileSync("fireblocks_secret.key");

// Read the cosigner public key (you can get it by running: ./cosigner print-public-key on the cosigner machine)
const cosignerPubKey = fs.readFileSync("publickey.pem");

// Start express app and set middleware 
const port = 8080;

const app = express();
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json())
app.use((req) => {
    req.rawBody = "";
    req.setEncoding("utf8");
    req.on("data", (chunk) => {
        req.rawBody += chunk;
    });
    req.on("end", () => {
        req.next();
    });
}
);

// Generate JWT response
const generateSignedResponse = (action) => {

    const signedRes = jwt.sign(
        action,
        privateKey,
        { algorithm: "RS256" }
    );

    return signedRes
}

// Verify that the params of the rawTx match the payload
const validateETHTransaction = (payload) => {

    const providedHash = payload.rawTx[0].payload
    // Decode RLP
    const unsignedTx = FeeMarketEIP1559Transaction.fromSerializedTx(
        Buffer.from(payload.rawTx[0].rawTx, "hex"))
    return (
        parseFloat(unsignedTx.value, 16) / 1e18 === payload.destinations[0].amountNative
        &&
        unsignedTx.to.toString("hex") === payload.destinations[0].displayDstAddress.toLowerCase()
        &&
        unsignedTx.getMessageToSign(true).toString("hex") == providedHash
    )
}

// Tx Sign Request endpoint 
app.post("/v2/tx_sign_request", (req, res) => {

    try {
        let response;
        const tx = jwt.verify(req.rawBody, cosignerPubKey);
        if (validateETHTransaction(tx)) {
            response = generateSignedResponse({
                action: "APPROVE",
                requestId: tx.requestId
            })
        } else {
            response = generateSignedResponse({
                action: "REJECT",
                requestId: tx.requestId,
                rejectionReason: `Failed to validate ETH transaction`
            })
        }

        res.status(200).send(response);

    } catch (e) {
        console.error(e)
        res.sendStatus(401).send();
    }
});

console.log(`Callback is running on http://localhost:${port}`)
app.listen(port);

Thank you for your help !