qtum-ethers-wrapper
v2.1.2
Published
A module for using Qtum through an Ethers compliant library to make it simpler to use Qtum
Downloads
40
Readme
Qtum Ethers
A module for using Qtum through an Ethers compliant library to make it simpler to use Qtum
TLDR for Ethereum developers
import {Contract} from "ethers"
import {
// import QtumProvider as Provider, replacement for ethers Provider
QtumProvider as Provider,
// import QtumWallet as Wallet, replacement for ethers Wallet
QtumWallet as Wallet,
// import QtumContractFactory as ContractFactory, replacement for ethers ContractFactory
QtumContractFactory as ContractFactory,
// Qtum has two bip44 derivation paths, wallets use different ones
// this is optional, the default is SLIP_BIP44_PATH
QTUM_BIP44_PATH, // Compatible with Qtum core wallet and electrum
SLIP_BIP44_PATH, // Compatible with 3rd party wallets
// Qtum uses compressed public/private keys and you need to consider them when doing cryptography
// these functions are replacements for ethers' ones
// they have an extra optional parameter to determine whether to use compressed or uncompressed keys (see below)
computeAddress,
recoverAddress,
// Qtum uses a different hash prefix for messages, use these ethers replacement functions
hashMessage,
messagePrefix
} from "qtum-ethers-wrapper";
// Qtum does not support nonces since it is a fork of Bitcoin
// there is an equivalent workaround feature built into this library
// it hashes the Bitcoin UTXO inputs and creates a 'nonce'
// you can get this nonce and use it to force usage of specific Bitcoin UTXO inputs
// (see documentation further below if idempotency is required)
const signer = new Wallet(
privkey,
provider,
{
// optional, will default to true in a future release
filterDust: true,
// optional, disable remembering which UTXOs we consume
// so that we can avoid trying to spend them again while
// new transactions are in the mempool trying to spend them.
// having this enabled lets the library send multiple
// transactions per block.
disableConsumingUtxos: true,
// optional, specify inputs to ignore when creating transactions
// this list can be created from a serialized hex transaction via
// QtumWallet#getIdempotentNonce.inputs
ignoreInputs: [''],
// list of inputs to force, throws if unable to use them (eg they are already spent)
inputs: [''],
// hash of inputs, throws if a transaction does not re-use the exact same inputs
nonce: '',
}
)
Installation
Open a console and run
npm install qtum-ethers-wrapper
Example
import {Contract} from "ethers"
import {
QtumProvider as Provider,
QtumWallet as Wallet,
QtumContractFactory as ContractFactory,
QTUM_BIP44_PATH, // Compatible with Qtum core wallet and electrum
SLIP_BIP44_PATH // Compatible with 3rd party wallets
} from "qtum-ethers-wrapper";
// point Qtum Provider at Janus node https://github.com/qtumproject/janus/
const mainnetProvider = new Provider("https://janus.qiswap.com/api/");
const testnetProvider = new Provider("https://testnet-janus.qiswap.com/api/");
// or deploy your own node locally with a regtest network
// see for a pre-built docker image https://hub.docker.com/r/qtump/janus
const regtestProvider = new Provider("http://localhost:23889");
// or register an account with qnode https://qnode.qtum.info
const provider = testnetProvider;
// create a wallet
const privkey = "99dda7e1a59655c9e02de8592be3b914df7df320e72ce04ccf0427f9a366ec6e"
const signer = new Wallet(
privkey,
provider,
{
// optional, will default to true in a future release
filterDust: true,
// optional, disable remembering which UTXOs we consume
// so that we can avoid trying to spend them again while
// new transactions are in the mempool trying to spend them.
// having this enabled lets the library send multiple
// transactions per block.
disableConsumingUtxos: true,
// optional, specify inputs to ignore when creating transactions
// this list can be created from a serialized hex transaction via
// QtumWallet#getIdempotentNonce.inputs
ignoreInputs: [''],
// list of inputs to force, throws if unable to use them (eg they are already spent)
inputs: [''],
// hash of inputs, throws if a transaction does not re-use the exact same inputs
nonce: '',
}
)
// or create a random account and get the mnemonic
// const signer = Wallet.createRandom(/*{ path = SLIP_BIP44_PATH }*/}).connect(provider);
// const {locale, path, phrase} = signer._mnemonic();
// QRC20 ABI
const ABI = [{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"","type":"bool"}],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"","type":"uint256"}],"payable":false,"stateMutability":"view","type":"function"},{"payable":true,"stateMutability":"payable","type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}];
// https://github.com/qtumproject/janus/blob/master/playground/pet-shop-tutorial/contracts/QRC20Token.sol
const BYTECODE = "0x60806040526100106008600a610141565b61001e90633b9aca00610154565b60005534801561002d57600080fd5b50600080543382526001602052604090912055610173565b634e487b7160e01b600052601160045260246000fd5b600181815b8085111561009657816000190482111561007c5761007c610045565b8085161561008957918102915b93841c9390800290610060565b509250929050565b6000826100ad5750600161013b565b816100ba5750600061013b565b81600181146100d057600281146100da576100f6565b600191505061013b565b60ff8411156100eb576100eb610045565b50506001821b61013b565b5060208310610133831016604e8410600b8410161715610119575081810a61013b565b610123838361005b565b806000190482111561013757610137610045565b0290505b92915050565b600061014d838361009e565b9392505050565b600081600019048311821515161561016e5761016e610045565b500290565b6106e0806101826000396000f3fe6080604052600436106100855760003560e01c806306fdde0314610094578063095ea7b3146100de57806318160ddd1461010e57806323b872dd14610132578063313ce567146101525780635a3b7e421461017957806370a08231146101ae57806395d89b41146101db578063a9059cbb1461020a578063dd62ed3e1461022a57600080fd5b3661008f57600080fd5b600080fd5b3480156100a057600080fd5b506100c860405180604001604052806008815260200167145490c8151154d560c21b81525081565b6040516100d5919061050a565b60405180910390f35b3480156100ea57600080fd5b506100fe6100f936600461057b565b610262565b60405190151581526020016100d5565b34801561011a57600080fd5b5061012460005481565b6040519081526020016100d5565b34801561013e57600080fd5b506100fe61014d3660046105a5565b610315565b34801561015e57600080fd5b50610167600881565b60405160ff90911681526020016100d5565b34801561018557600080fd5b506100c860405180604001604052806009815260200168546f6b656e20302e3160b81b81525081565b3480156101ba57600080fd5b506101246101c93660046105e1565b60016020526000908152604090205481565b3480156101e757600080fd5b506100c86040518060400160405280600381526020016251544360e81b81525081565b34801561021657600080fd5b506100fe61022536600461057b565b61042d565b34801561023657600080fd5b506101246102453660046105fc565b600260209081526000928352604080842090915290825290205481565b6000826001600160a01b03811661027857600080fd5b8215806102a657503360009081526002602090815260408083206001600160a01b0388168452909152902054155b6102af57600080fd5b3360008181526002602090815260408083206001600160a01b03891680855290835292819020879055518681529192917f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b92591015b60405180910390a35060019392505050565b6000836001600160a01b03811661032b57600080fd5b836001600160a01b03811661033f57600080fd5b6001600160a01b038616600090815260026020908152604080832033845290915290205461036d90856104c8565b6001600160a01b0387166000818152600260209081526040808320338452825280832094909455918152600190915220546103a890856104c8565b6001600160a01b0380881660009081526001602052604080822093909355908716815220546103d790856104eb565b6001600160a01b03808716600081815260016020526040908190209390935591519088169060008051602061068b833981519152906104199088815260200190565b60405180910390a350600195945050505050565b6000826001600160a01b03811661044357600080fd5b3360009081526001602052604090205461045d90846104c8565b33600090815260016020526040808220929092556001600160a01b0386168152205461048990846104eb565b6001600160a01b03851660008181526001602052604090819020929092559051339060008051602061068b833981519152906103039087815260200190565b6000818310156104da576104da61062f565b6104e4828461065b565b9392505050565b6000806104f88385610672565b9050838110156104e4576104e461062f565b600060208083528351808285015260005b818110156105375785810183015185820160400152820161051b565b81811115610549576000604083870101525b50601f01601f1916929092016040019392505050565b80356001600160a01b038116811461057657600080fd5b919050565b6000806040838503121561058e57600080fd5b6105978361055f565b946020939093013593505050565b6000806000606084860312156105ba57600080fd5b6105c38461055f565b92506105d16020850161055f565b9150604084013590509250925092565b6000602082840312156105f357600080fd5b6104e48261055f565b6000806040838503121561060f57600080fd5b6106188361055f565b91506106266020840161055f565b90509250929050565b634e487b7160e01b600052600160045260246000fd5b634e487b7160e01b600052601160045260246000fd5b60008282101561066d5761066d610645565b500390565b6000821982111561068557610685610645565b50019056feddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3efa264697066735822122002051c03b9e6486c5b21b4c14e6ea0627a710175b0142fc40ce30042333632a464736f6c634300080a0033"
// QtumContractFactory is required to be used instead of standard ethers ContractFactory due to how contract addresses are computed differently
const simpleStore = new ContractFactory(ABI, BYTECODE, signer);
// simpleStore deployment example, returns address
async function deployToken() {
const deployment = await simpleStore.deploy({
gasLimit: "0x7a120", // 500,000
gasPrice: "0x190" // in WEI OR Satoshis
});
await deployment.deployed();
return deployment.address
}
// connect to QRC20 token and interact with it
async function transferToken(contractAddress, from, to, value) {
const qrc20 = new Contract(contractAddress, QRC_ABI, signer)
const name = await qrc20.transfer(from, to, value,
{
gasLimit: "0x62521", // 62521
gasPrice: "0x5d21dba000", // in WEI OR Satoshis (0x190)
}
);
}
const contractAddress = await deployToken();
await transferToken(contractAddress, "0x...", "0x...", 1);
// sending QTUM
await signer.sendTransaction({
to: "0x7926223070547D2D15b2eF5e7383E541c338FfE9",
from: signer.address,
gasLimit: "0x3d090",
gasPrice: "0x190",
// in Satoshis
value: "0xfffff",
data: "",
});
Signing/recovering messages
QTUM uses compressed public keys to generate addresses so you need to use our modified recoverAddress
instead of ethers.utils.recoverAddress
.
Uncompressed keys are supported as well, it uses the recovery parameter to identify if an uncompressed key was used.
Hash message also uses a different message prefix than Ethereum, it uses \15QTUM Signed Message:\n
VRS Signature format
Ethereum serializes signautres as RSV while Bitcoin/Qtum uses VRS, this library supports both formats. The signatures are identical except for how they are serialized, they reference the same points on the elliptic curve.
import {
computeAddress,
hashMessage,
messagePrefix,
recoverAddress,
recoverAddressBtc,
} from "qtum-ethers-wrapper";
const message = "1234";
const digest = hashMessage(message);
const signedMessageRSV = await signer.signMessage(message);
const signedMessageVRS = await signer.signMessageBtc(message);
const recoveredRSV = recoverAddress(digest, signedMessageRSV);
const recoveredVRS = recoverAddressBtc(digest, signedMessageVRS);
if (recoveredRSV !== recoveredVRS) {
throw new Error("Expected identical addresses");
}
Idempotency
Idempotency in Bitcoin forks involves tying logic to specific UTXO inputs or re-sending the raw serialized transaction and re-crafting a new transaction if that one fails.
This can be done by specifying inputs to use and a special nonce.
The nonce is a hash of each UTXO input in the created transaction.
You will need to keep track of what inputs are attached to what transaction and you can continue sending the transaction
const tx = await signer.sendTransaction({
to: "0x7926223070547D2D15b2eF5e7383E541c338FfE9",
from: signer.address,
gasLimit: "0x3d090",
gasPrice: "0x190",
// in Satoshis
value: "0xfffff",
data: "",
});
console.log("Generated hash of inputs:", tx.nonce);
console.log("Inputs of transaction:", JSON.stringify(tx.inputs));
console.log("bitcoinjs-lib decoded transaction:", tx.decoded);
console.log("raw serialized signed transaction:", tx.signedTransaction);
// save the signed transaction to your database
// you can re-send the signed transaction as many times as you want and it will always be idempotent
// send the transaction and get a transaction response
const transactionResponse = await tx.sendTransaction();
// re-send the raw signed transaction
const transactionResponse = await provider.sendTransaction(tx.signedTransaction);
// create a transaction while requiring specific inputs
const txWithoutInputRequirements = await signer.sendTransactionIdempotent({
to: "0x7926223070547D2D15b2eF5e7383E541c338FfE9",
from: signer.address,
gasLimit: "0x3d090",
gasPrice: "0x190",
// in Satoshis
value: "0xfffff",
data: "",
});
console.log("Created transaction that uses these inputs:", JSON.stringify(txWithoutInputRequirements.inputs));
console.log("Use this nonce to throw if the exact same inputs are not used:", txWithoutInputRequirements.nonce);
const txWithInputRequirement = await signer.sendTransactionIdempotent({
to: "0x7926223070547D2D15b2eF5e7383E541c338FfE9",
from: signer.address,
gasLimit: "0x3d090",
gasPrice: "0x190",
// in Satoshis
value: "0xfffff",
data: "",
// you can specify inputs here or when creating an instance of QtumWallet
inputs: txWithoutInputRequirements.inputs,
// throw unless inputs match exactly
nonce: txWithoutInputRequirements.nonce,
});
const txReceipt = await txWithInputRequirement.sendTransaction();
Notes
- Issues
Qtum estimate gas function is not perfect so eth_estimateGas has a 20% buffer for gas limit
Janus doesn't return a transaction receipt for p2pkh tx's
This extension works with p2pk and p2pkh scripts only and asks Janus for p2pk and p2pkh scripts only