2

I have a server-client DApp that I've tested working fine on an Ethereum test network. But due to gas fees, I want to use a L2, in this case I chose Polygon (MATIC). Basic app is reading and writing text posts to a website, the smart contract stores them.

I have successfully deployed on MATIC using remix.ethereum.org, and from Remix I can write transactions to the contract. On my localhost web app, I can read transactions, but my writing is not working from the client.

Here is the server.js

const WEB3_PROVIDER = "https://polygon-rpc.com" 
// https://blog.polygon.technology/polygon-rpc-gateway-will-provide-a-free-high-performance-connection-to-the-polygon-pos-blockchain/

//"https://cloudflare-eth.com"; //"HTTP://127.0.0.1:7545"
if (typeof web3 !== 'undefined') {
    web3 = new Web3(web3.currentProvider);
    console.log("web3 already initialized.");
} else {
    // set the provider you want from Web3.providers
    web3 = new Web3(new Web3.providers.HttpProvider(WEB3_PROVIDER));
    console.log("New web3 object initialized.");
}

app.post('/add-post', async (req, res) => {
    const post = req.body;
    
    try {
        console.log(post);

        MyContract.methods.addNewPost(post['name'], post['post'], post['date']).send({from: post['addr'], gas:3000000}).then(function(result) {
            const output_response = "add-post successful :: ";//+String(result);
            res.send(output_response);
        }).catch(function(err) {
            const output_response = "add-post failed :: "+String(err);
            res.send(output_response);
        });
        
    } catch (e) { throw e; }
});

And here is the snippet in client.js where I am adding a post, by grabbing the html input form and then passing to the following:

const web3 = new Web3(window.ethereum);

async function addPost(post_input) {
    stringify_post_input = JSON.stringify(post_input);
    const post_response = await fetch('/add-post', {method: 'POST', body: stringify_post_input, headers: { "content-type": "application/json" } });
    var post_response_text = await post_response.text();
    console.log(post_response_text);
}

Now this usually works flawlessly on ethereum test network, where all I change is the web3 initialization in server.js. But now on the MATIC network I get, in my client browser,

add-post failed :: Error: Returned error: unknown account

This is really confusing to me, because

  1. I can manually add posts in remix.ethereum.org, where I deployed this exact same MyContract
  2. I have other server-side calls that read from MyContract and work fine (i.e. I can read existing posts I added from Remix).

So my client can read but not write, i.e. no MetaMask pop-up asking me to confirm to pay gas fees.

This is my first time trying to use a L2, so I have no idea if all the web3 code should be the same. I've been under the impression that I only need to swap the networks and log into my MetaMask and it should all be fine. But I don't really understand web3 that deeply, so I'm not sure.

Help much appreciate - ideally when I try and write with MyContract.methods...(), I should get a MetaMask pop-up in my client browser asking me to confirm paying gas fees.

TylerH
  • 20,799
  • 66
  • 75
  • 101
JDS
  • 16,388
  • 47
  • 161
  • 224

1 Answers1

3
MyContract.methods.addNewPost(...).send({from: post['addr'], gas:3000000})

This snippet generates a transaction from the post['addr'] address, and sends the generated transaction to the node. When web3 instance holds the corresponding private key to this address, it signs the transaction before sending.

When web3 doesn't hold the private key, it sends the transaction unsigned. Which is useful only in dev environment (e.g. the Remix VM emulator, Ganache, Hardhat, ...) where the node might hold the private key and still sign the transaction. But in this case, the node doesn't have the corresponding private key and returns the "unknown account" error.


Your question already suggests a solution - request MetaMask (or another wallet software holding the user's private key) to sign the transaction instead of signing it with web3 or on the node.

This requires moving the transaction generating logic to a frontend app (in your case client.js), so that the frontend app can communicate with the MetaMask API available throuth the window.ethereum object (docs), which is logically not available in the backend app.

client.js:

// containing the contract `addPost` function definition
const abiJson = [{
    "inputs": [
        {"internalType": "string", "name": "_name", "type": "string"},
        {"internalType": "string", "name": "_post", "type": "string"},
        {"internalType": "string", "name": "_date", "type": "string"}
    ],
    "name": "addPost", "outputs": [], "stateMutability": "nonpayable", "type": "function"
}];
const contractAddress = "0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF";

let accounts, web3, contract;

async function init() {
    // popup - get the user's address
    accounts = await ethereum.request({ method: 'eth_requestAccounts' });

    // using web3 just as a helper to generate the transaction
    // (see the `data` field and `encodeABI`) - not to sign it
    web3 = new Web3();
    contract = new web3.eth.Contract(abiJson);
}

async function addPost(postInput) {
    const transactionParameters = {
        from: accounts[0],
        to: contractAddress,
        data: contract.methods.addPost(
            postInput.name,
            postInput.post,
            postInput.date
        ).encodeABI(),
        gasPrice: '0x09184e72a000', // custom gas price
    };
    // popup - request the user to sign and broadcast the transaction
    await ethereum.request({
        method: 'eth_sendTransaction',
        params: [transactionParameters],
    });
}

async function run() {
    await init();
    await addPost({
        name: "This is a post name",
        post: "Hello world",
        date: "date"
    });
}

run();

Now you can safely remove the endpoint, as it's not needed anymore.

Petr Hejda
  • 40,554
  • 8
  • 72
  • 100
  • Wow fantastic @Petr! This made it work, I will accept your answer. I just have 2 quick follow-up Qs if you got a minute. 1) If I can just call my contract methods from the frontend like this, do I really need any `web3`/`eth` functionality in my backend (server.js)? It seems I can have it all in client. And 2), is there any way to specify gas fees the user should pay, so they can have their post uploaded faster? See this image on what happens in MetaMask for my post update: https://imgur.com/6uW11vc – JDS Feb 07 '22 at 19:03
  • 1
    1) Your use case doesn't seem to require any interaction from the backend. Generally, backend interacts with a smart contract when you're automating actions on **your own** account (e.g. a trading bot, or an oracle app passing data to the contract from a specific authorized address). – Petr Hejda Feb 07 '22 at 20:12
  • 1
    2) You can set the absolute value of gas price which is used in calculation: _(price in wei * gas limit = total gas fees in MATIC)_. It needs to be passed in **hex** format as a property of the `transactionParameters` object. See the `gasPrice` [example in docs](https://docs.metamask.io/guide/sending-transactions.html#sending-transactions) or the updated answer with the added `gasPrice` field. – Petr Hejda Feb 07 '22 at 20:12
  • That all makes a ton of sense, cheers! – JDS Feb 07 '22 at 20:16