swaponline / swap.io-app

https://app.swap.io
https://app.swap.io
7 stars 13 forks source link

Create a simple wallet interface #50

Open vladiuz1 opened 3 years ago

vladiuz1 commented 3 years ago

https://iancoleman.io/bip39/

You will need to study how wallets are created in the above implementation.

In order to create a wallet, first you need to create a profile, which in essense is a mnemonic phrase saved in local cookies.

The phrase is used to derive HDkey and from there subkeys for every supported blockchain, including bitcoin, ethereum, even things like handshake and binance chain.

Once a profile is created a user needs to select currency he want to create a wallet for, and an address will be created based on the above example. The website can be used to generate test cases to make sure the addresses are created in the correct format for every blockchain.

Ethereum-based blockchains do not always require creating a new address when adding a wallet.

When adding a erc20 token wallet no new address will be generated, the ETH addresses will be reused.

As you may know, in Ethereum one address can hold multiple currencies (eth + tokens). One address can hold ETH, but also USDT, WBTC, UNI and thousands of other popular tokens. We could generate of course a new address for every currency using the key derivation mechanism, but this is not practical, because transactions in these token currencies require ETH balances for fees required to send the tokens.

So its important to destinguish between classic utxo crypt from token currencies that require to have an address in the main currency on the same address.

image

So if we added a new WBTC wallet, we would in effect add 3 WBTC wallets for each of the ETH address that we already have setup. And they would have the same label as ETH addresses.

Changing the label for one ETH-based wallet in one crypto with shared address will change the label for other ETH based crypto too.

There is also a case of multiblockchain address share.

For example Avalance C-blockchain has the same network definitions as Ethereum, and generating address in Ethereum will also have the same address in Avalanche blockchain. This may be important for cross chain transactions to have the same address in both blockchain. Hence when we have such cases we need to stick to the rule that a wallet is an address, and an address can have one label, but an address can be used by multiple wallets (even blockchains with the same network definitions).

Another important extension is token labeling.

For example multiple blockchains have WBTC token.

For example Binance Blockchain and Ethereum both have the WBTC token - a wrapped bitcoin. When token labels are displayed, its very important to label them with the relevant blockchain.

I suggest we label it with subicon.

Main icon is the token icon and subicon for the blockchain it is on

wbtc2

vladiuz1 commented 3 years ago

If you look at the source code of Ian Colman's website you will see that he makes a few exceptions to standard bitcoin core library for generation of addresses and representation of keys:

// From: view-source:https://iancoleman.io/bip39/

                // Ethereum values are different
                if (networkIsEthereum()) {
                    var pubkeyBuffer = keyPair.getPublicKeyBuffer();
                    var ethPubkey = libs.ethUtil.importPublic(pubkeyBuffer);
                    var addressBuffer = libs.ethUtil.publicToAddress(ethPubkey);
                    var hexAddress = addressBuffer.toString('hex');
                    var checksumAddress = libs.ethUtil.toChecksumAddress(hexAddress);
                    address = libs.ethUtil.addHexPrefix(checksumAddress);
                    pubkey = libs.ethUtil.addHexPrefix(pubkey);
                    if (hasPrivkey) {
                        privkey = libs.ethUtil.bufferToHex(keyPair.d.toBuffer(32));
                    }
                }
                //TRX is different
                if (networks[DOM.network.val()].name == "TRX - Tron") {
                    keyPair = new libs.bitcoin.ECPair(keyPair.d, null, { network: network, compressed: false });
                    var pubkeyBuffer = keyPair.getPublicKeyBuffer();
                    var ethPubkey = libs.ethUtil.importPublic(pubkeyBuffer);
                    var addressBuffer = libs.ethUtil.publicToAddress(ethPubkey);
                    address = libs.bitcoin.address.toBase58Check(addressBuffer, 0x41);
                    if (hasPrivkey) {
                        privkey = keyPair.d.toBuffer().toString('hex');
                    }
                }

                // RSK values are different
                if (networkIsRsk()) {
                    var pubkeyBuffer = keyPair.getPublicKeyBuffer();
                    var ethPubkey = libs.ethUtil.importPublic(pubkeyBuffer);
                    var addressBuffer = libs.ethUtil.publicToAddress(ethPubkey);
                    var hexAddress = addressBuffer.toString('hex');
                    // Use chainId based on selected network
                    // Ref: https://developers.rsk.co/rsk/architecture/account-based/#chainid
                    var chainId;
                    var rskNetworkName = networks[DOM.network.val()].name;
                    switch (rskNetworkName) {
                        case "R-BTC - RSK":
                            chainId = 30;
                            break;
                        case "tR-BTC - RSK Testnet":
                            chainId = 31;
                            break;
                        default:
                            chainId = null;
                    }
                    var checksumAddress = toChecksumAddressForRsk(hexAddress, chainId);
                    address = libs.ethUtil.addHexPrefix(checksumAddress);
                    pubkey = libs.ethUtil.addHexPrefix(pubkey);
                    if (hasPrivkey) {
                        privkey = libs.ethUtil.bufferToHex(keyPair.d.toBuffer());
                    }
                }

                // Handshake values are different
                if (networks[DOM.network.val()].name == "HNS - Handshake") {
                    var ring = libs.handshake.KeyRing.fromPublic(keyPair.getPublicKeyBuffer())
                    address = ring.getAddress().toString();
                }

                // Stellar is different
                if (networks[DOM.network.val()].name == "XLM - Stellar") {
                    var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
                    var coin = parseIntNoNaN(DOM.bip44coin.val(), 0);
                    var path = "m/";
                        path += purpose + "'/";
                        path += coin + "'/" + index + "'";
                    var keypair = libs.stellarUtil.getKeypair(path, seed);
                    indexText = path;
                    privkey = keypair.secret();
                    pubkey = address = keypair.publicKey();
                }

                // Nano currency
                if (networks[DOM.network.val()].name == "NANO - Nano") {
                    var nanoKeypair = libs.nanoUtil.getKeypair(index, seed);
                    privkey = nanoKeypair.privKey;
                    pubkey = nanoKeypair.pubKey;
                    address = nanoKeypair.address;
                }

                if ((networks[DOM.network.val()].name == "NAS - Nebulas")) {
                    var privKeyBuffer = keyPair.d.toBuffer(32);
                    var nebulasAccount = libs.nebulas.Account.NewAccount();
                    nebulasAccount.setPrivateKey(privKeyBuffer);
                    address = nebulasAccount.getAddressString();
                    privkey = nebulasAccount.getPrivateKeyString();
                    pubkey = nebulasAccount.getPublicKeyString();
                }
                // Ripple values are different
                if (networks[DOM.network.val()].name == "XRP - Ripple") {
                    privkey = convertRipplePriv(privkey);
                    address = convertRippleAdrr(address);
                }
                // Jingtum values are different
                if (networks[DOM.network.val()].name == "SWTC - Jingtum") {
                    privkey = convertJingtumPriv(privkey);
                    address = convertJingtumAdrr(address);
                }
                // CasinoCoin values are different
                if (networks[DOM.network.val()].name == "CSC - CasinoCoin") {
                    privkey = convertCasinoCoinPriv(privkey);
                    address = convertCasinoCoinAdrr(address);
                }
                // Bitcoin Cash address format may vary
                if (networks[DOM.network.val()].name == "BCH - Bitcoin Cash") {
                    var bchAddrType = DOM.bitcoinCashAddressType.filter(":checked").val();
                    if (bchAddrType == "cashaddr") {
                        address = libs.bchaddr.toCashAddress(address);
                    }
                    else if (bchAddrType == "bitpay") {
                        address = libs.bchaddr.toBitpayAddress(address);
                    }
                }
                 // Bitcoin Cash address format may vary
                 if (networks[DOM.network.val()].name == "SLP - Simple Ledger Protocol") {
                     var bchAddrType = DOM.bitcoinCashAddressType.filter(":checked").val();
                     if (bchAddrType == "cashaddr") {
                         address = libs.bchaddrSlp.toSlpAddress(address);
                     }
                 }

                // ZooBC address format may vary
                if (networks[DOM.network.val()].name == "ZBC - ZooBlockchain") {  

                    var purpose = parseIntNoNaN(DOM.bip44purpose.val(), 44);
                    var coin = parseIntNoNaN(DOM.bip44coin.val(), 0);
                    var path = "m/";
                        path += purpose + "'/";
                        path += coin + "'/" + index + "'";
                    var result = libs.zoobcUtil.getKeypair(path, seed);

                    let publicKey = result.pubKey.slice(1, 33);
                    let privateKey = result.key;

                    privkey = privateKey.toString('hex');
                    pubkey = publicKey.toString('hex');

                    indexText = path;
                    address = libs.zoobcUtil.getZBCAddress(publicKey, 'ZBC');
                }

                // Segwit addresses are different
                if (isSegwit) {
                    if (!segwitAvailable) {
                        return;
                    }
                    if (isP2wpkh) {
                        var keyhash = libs.bitcoin.crypto.hash160(key.getPublicKeyBuffer());
                        var scriptpubkey = libs.bitcoin.script.witnessPubKeyHash.output.encode(keyhash);
                        address = libs.bitcoin.address.fromOutputScript(scriptpubkey, network)
                    }
                    else if (isP2wpkhInP2sh) {
                        var keyhash = libs.bitcoin.crypto.hash160(key.getPublicKeyBuffer());
                        var scriptsig = libs.bitcoin.script.witnessPubKeyHash.output.encode(keyhash);
                        var addressbytes = libs.bitcoin.crypto.hash160(scriptsig);
                        var scriptpubkey = libs.bitcoin.script.scriptHash.output.encode(addressbytes);
                        address = libs.bitcoin.address.fromOutputScript(scriptpubkey, network)
                    }
                    else if (isP2wsh) {
                        // https://github.com/libs.bitcoinjs-lib/blob/v3.3.2/test/integration/addresses.js#L71
                        // This is a 1-of-1
                        var witnessScript = libs.bitcoin.script.multisig.output.encode(1, [key.getPublicKeyBuffer()]);
                        var scriptPubKey = libs.bitcoin.script.witnessScriptHash.output.encode(libs.bitcoin.crypto.sha256(witnessScript));
                        address = libs.bitcoin.address.fromOutputScript(scriptPubKey, network);
                    }
                    else if (isP2wshInP2sh) {
                        // https://github.com/libs.bitcoinjs-lib/blob/v3.3.2/test/integration/transactions.js#L183
                        // This is a 1-of-1
                        var witnessScript = libs.bitcoin.script.multisig.output.encode(1, [key.getPublicKeyBuffer()]);
                        var redeemScript = libs.bitcoin.script.witnessScriptHash.output.encode(libs.bitcoin.crypto.sha256(witnessScript));
                        var scriptPubKey = libs.bitcoin.script.scriptHash.output.encode(libs.bitcoin.crypto.hash160(redeemScript));
                        address = libs.bitcoin.address.fromOutputScript(scriptPubKey, network)
                    }
                }

                if ((networks[DOM.network.val()].name == "CRW - Crown")) {
                    address = libs.bitcoin.networks.crown.toNewAddress(address);
                }

              if (networks[DOM.network.val()].name == "EOS - EOSIO") {
                    address = ""
                    pubkey = EOSbufferToPublic(keyPair.getPublicKeyBuffer());
                    privkey = EOSbufferToPrivate(keyPair.d.toBuffer(32));
                }

                if (networks[DOM.network.val()].name == "FIO - Foundation for Interwallet Operability") {
                    address = ""
                    pubkey = FIObufferToPublic(keyPair.getPublicKeyBuffer());
                    privkey = FIObufferToPrivate(keyPair.d.toBuffer(32));
                }

                if (networks[DOM.network.val()].name == "ATOM - Cosmos Hub") {
                    address = CosmosBufferToAddress(keyPair.getPublicKeyBuffer());
                    pubkey = CosmosBufferToPublic(keyPair.getPublicKeyBuffer());
                    privkey = keyPair.d.toBuffer().toString("base64");
                }

                //Groestlcoin Addresses are different
                if(isGRS()) {

                    if (isSegwit) {
                        if (!segwitAvailable) {
                            return;
                        }
                        if (isP2wpkh) {
                            address = libs.groestlcoinjs.address.fromOutputScript(scriptpubkey, network)
                        }
                        else if (isP2wpkhInP2sh) {
                            address = libs.groestlcoinjs.address.fromOutputScript(scriptpubkey, network)
                        }
                    }
                    //non-segwit addresses are handled by using groestlcoinjs for bip32RootKey
                }

                if (isELA()) {
                    let elaAddress = calcAddressForELA(
                        seed,
                        parseIntNoNaN(DOM.bip44coin.val(), 0),
                        parseIntNoNaN(DOM.bip44account.val(), 0),
                        parseIntNoNaN(DOM.bip44change.val(), 0),
                        index
                    );
                    address = elaAddress.address;
                    privkey = elaAddress.privateKey;
                    pubkey = elaAddress.publicKey;
                }

                addAddressToList(indexText, address, pubkey, privkey);
                if (isLast) {
                    hidePending();
                    updateCsv();
                }
            }, 50)
        }

These differences need to be systematic so that they apply to some kind of network definitions json object, where every blockchain network is defined with all the different parameters.

a bitcoinlib module has the following network definition format:

{
  "dash": {
    "bip44_cointype": 5,
    "currency_code": "DASH",
    "currency_name": "dash",
    "currency_name_plural": "dash coins",
    "currency_symbol": "DASH",
    "denominator": 1e-08,
    "description": "Dash Network",
    "dust_amount": 1000,
    "fee_default": 2000,
    "fee_max": 50000,
    "fee_min": 1024,
    "prefix_address": "4C",
    "prefix_address_p2sh": "10",
    "prefix_bech32": "dash",
    "prefix_wif": "CC",
    "prefixes_wif": [
      [
        "0488B21E",
        "xpub",
        "public",
        false,
        "legacy",
        "p2pkh"
      ],
      [
        "0488B21E",
        "xpub",
        "public",
        true,
        "legacy",
        "p2sh"
      ],
      [
        "0488ADE4",
        "xprv",
        "private",
        false,
        "legacy",
        "p2pkh"
      ],
      [
        "0488ADE4",
        "xprv",
        "private",
        true,
        "legacy",
        "p2sh"
      ]
    ],
    "priority": 10
  }
}

We only need those parameters that are important to our case.

For example we really don't care about the format the private keys are stored, all of our private keys will be derived on the fly hence we will never store the derived keys, only the mnemonic phrase. Hence we don't need to know the private key format prefixes and wether or not they are extended. However, we do need to know symbols, message signing prefixes, denomination values, address format specifics.

A json object describing each network is a must.

We need to keep in mind that blockchains may have multiple testnets (ethereum's ropsten, goerli, etc..) and they all are separate networks and need to be defined as a separate network definition object.

The network definition object will need to be used to put thru our library based on Ian Colman's library plus web3 and may be some other libraries.

We care about the following minimum functionality of the frontend library:

The he rest of the functionality is not important to us.

We can outsource the serialisation of transactions to trusted nodes, we don't need to store private keys in specific formats, we only need to know how to derive private keys in their basic representation, calculate the addresses according to network definitions, sign messages and transactions. Even the serialisation of transactions can be outsourced to a trusted node to keep the frontend code to minimum.