klaytn / caver-js

Official caver-js repository
GNU Lesser General Public License v3.0
114 stars 75 forks source link

Cannot send contract method: failed to send a raw transaction to klaytn node; invalid transaction v, r, s values of the sender #562

Closed marvinirwin closed 2 years ago

marvinirwin commented 2 years ago

Describe the bug

I can't seem to call contract.methods.someMethod().send() or contract.methods.someMethod().sign() without getting an invalid transaction v, r, s values of the sender error.

I made the following script to show an extremely minimal example. It seems like it should let me deploy and interact with the contract, but I it produces the above error.

const Caver = require('caver-js');
const fetch = require('node-fetch');
const CaverExtKAS = require('caver-js-ext-kas')
const readline = require('readline').createInterface({
    input: process.stdin,
    output: process.stdout
});
const fs = require('fs');
const btoa = str => Buffer.from(str).toString('base64');

const readLine = prompt => {
    return new Promise(resolve => {
        readline.question(prompt, name => {
            readline.close();
            resolve(name)
        });
    })
}

const getRequiredEnvironmentVariables = (...keys) => {
    const unsetVariables = keys.filter(key => !process.env[key]);
    if (unsetVariables.length) {
        throw new Error(`The following environment variables are not set ${unsetVariables.join(', ')}`)
    }
    const variables = Object.fromEntries(keys.map(key => [key, process.env[key]]));
    console.log(JSON.stringify(variables, null, '  '));
    return variables
}

const resolveChainId = (key) => {
    /**
     * https://refs.klaytnapi.com/en/wallet/latest#operation/ContractDeployTransaction
     */
    const chainMap = {
        baobab: 1001,
        cypress: 8217
    }
    const chainId = chainMap[key];
    if (!chainId) {
        throw new Error(`Unknown chain: ${key}`);
    }
    return chainId
}

const {
    KLAYTN_NETWORK,
    KLAYTN_GAS,
    KLAYTN_CHAIN,
    KAS_PUBLIC_KEY,
    KAS_PRIVATE_KEY,
    KLAYTN_COMPILED_CONTRACT_PATH,
} = getRequiredEnvironmentVariables(
    "KLAYTN_NETWORK",
    "KLAYTN_GAS",
    "KLAYTN_CHAIN",
    "KAS_PUBLIC_KEY",
    "KAS_PRIVATE_KEY",
    "KLAYTN_COMPILED_CONTRACT_PATH"
)
const chainId = resolveChainId(KLAYTN_CHAIN);
const caver = new Caver(KLAYTN_NETWORK);
const caverKAS = new CaverExtKAS(chainId, KAS_PUBLIC_KEY, KAS_PRIVATE_KEY);

const resolveKeys = async () => {
    const cachedKeysPath = "cached-keyring.json";
    if (!fs.existsSync(cachedKeysPath)) {
        const keyring = await caver.wallet.keyring.generate();
        caver.wallet.add(keyring);
        console.log(keyring);
        await readLine("Please put some KLAY in the wallet from https://baobab.wallet.klaytn.com/faucet and then press enter");
        const klaytnAccount = keyring._address;
        const klaytnPrivateKey = keyring._key._privateKey;
        await caverKAS.kas.wallet.migrateAccounts([{
            address: klaytnAccount,
            key: klaytnPrivateKey,
            nonce: 0
        }]).then(console.log);
        const keys = {klaytnAccount, klaytnPrivateKey};
        fs.writeFileSync(cachedKeysPath, JSON.stringify(keys));
        return keys
    } else {
        return JSON.parse(fs.readFileSync(cachedKeysPath).toString());
    }
};
const deployContract = ({from, input}) => {
    const chainId = resolveChainId(KLAYTN_CHAIN);
    return fetch("https://wallet-api.klaytnapi.com/v2/tx/contract/deploy", {
        method: "POST",
        headers: {
            'x-chain-id': chainId,
            'Content-Type': 'application/json',
            'Authorization': `Basic ${btoa(`${KAS_PUBLIC_KEY}:${KAS_PRIVATE_KEY}`)}`,
        },
        body: JSON.stringify({
            from, // 'from' wallet address must be created via API! (https://refs.klaytnapi.com/en/wallet/latest#section/Authentication)
            value: "0x0",
            input,
            nonce: 0,
            gasLimit: KLAYTN_GAS,
            submit: true
        })
    });
}

(async () => {
    try {
        const {klaytnAccount, klaytnPrivateKey} = await resolveKeys();
        caver.wallet.newKeyring(klaytnAccount, klaytnPrivateKey)
        await deployContract({from: klaytnAccount, input: require(KLAYTN_COMPILED_CONTRACT_PATH).bytecode}).then(response => response.json()).then(console.log);
        const contractAddress = await readLine("Please input the contract address and press enter (Look up the transaction here https://baobab.scope.klaytn.com/)");
        await caverKAS.kas.wallet.migrateAccounts([{ address: klaytnAccount, key: klaytnPrivateKey, nonce: 0 }]).then(console.log);
        const contract = caver.contract.create(require(KLAYTN_COMPILED_CONTRACT_PATH).abi, contractAddress, {from: klaytnAccount, gas: KLAYTN_GAS});
        /**
         * This can be any contract method
         */
        const startSale = contract.methods.startSale()
        await startSale.sign({from: klaytnAccount, gas: KLAYTN_GAS}).then(console.log);/*send({gas: KLAYTN_GAS, from: klaytnAccount}).then(console.log)*/
    } catch(e) {
        console.error(e);
    }
})()

Expected behavior It should be able to execute the method.

jimni1222 commented 2 years ago

Hello, First of all, you can use all the features provided by caver-js by installing only one caver-js-ext-kas. So there is no need to use caver-js and caver-js-ext-kas at the same time. Please install and use only caver-js-ext-kas.

In addition, caver.kas.wallet.migrateAccounts is an API that migrates the account you are using so that it can be used in the KAS Wallet API. After migration, the private key of the Klaytn account is managed by KAS and not provided to users. When KAS receives the migration request, it sends an Account Update transaction to the key managed by KAS using the received address and key. This is why invalid transaction v, r, s values of the sender error is hannped. Therefore, the klaytnPrivateKey passed to the API is used to sign the transaction to update the account key, and klaytnPrivateKey cannot be used after the account key is updated.

Looking at the code you posted, it seems to send a migration request by creating a new key ring to be used in the KAS Wallet API. In this case, you can simply use caver.kas.wallet.createAccount(). Migration is only used when you want to use your existing Klaytn Account through the KAS Wallet API.

If an account has been added to the KAS Wallet API, you can deploy/execute the contract using the KAS Wallet API account by using the caver.contract provided by caver-js-ext-kas. Please see this documentation for details.

And since this question is an issue of caver-js-ext-kas, not caver-js, please ask additional questions in the relevant repo.