GardenBlockchain

A Dogecoin testnet wallet and transaction notebook

An older TypeScript PSBT experiment with derivation, P2PKH inputs, fee estimates, and broadcast handling.

The original Dogecoin note consisted of a TypeScript utility for testnet. It generated a mnemonic-derived wallet, constructed a legacy P2PKH payment, fetched UTXOs and previous transactions, then built and signed a PSBT.

The recorded source

Read the historical Dogecoin utility
/**
 * https://forum.dogecoin.org/d/60-doge-testnet-tooling-faucet--public-electrs-api--block-explorer--web-wallet
 * Wallet: https://testnet-wallet.doge.toys
 * Explorer: https://doge-testnet-explorer.qed.me
 * API: https://doge-electrs-testnet-demo.qed.me
 * Faucet: https://faucet.doge.toys
 * Libs:
 * - "@dogiwallet/dogecoinjs-lib": "^0.1.4",
 * - "bip32": "^5.0.0-rc.0",
 * - "bip39": "^3.1.0",
 * - "ecpair": "^3.0.0",
 * - "tiny-secp256k1": "^2.2.3"
 */
import * as dogecoin from '@dogiwallet/dogecoinjs-lib';
import axios from 'axios';
import BIP32Factory from 'bip32';
import * as bip39 from 'bip39';
import { ECPairFactory } from 'ecpair';
import * as ecc from 'tiny-secp256k1';

const ECPair = ECPairFactory(ecc);

const ONE_DOGE = 100_000_000; // 1 DOGE in satoshis
const network = dogecoin.networks.dogecoinTestnet;

/**
 * Create a new Dogecoin Wallet (KeyPair) and return the WIF private key and public address
 */
export function createDogeWallet() {
  // Generate new key pair
  // const keyPair = ECPair.makeRandom({ network: dogecoin.networks.dogecoinTestnet });
  // Get WIF private key
  // const privateKeyWIF = keyPair.toWIF();
  const bip32 = BIP32Factory(ecc);

  const mnemonic = bip39.generateMnemonic();
  const seed = bip39.mnemonicToSeedSync(mnemonic);
  const root = bip32.fromSeed(seed, network);
  const child = root.derivePath(`m/44'/${network === dogecoin.networks.dogecoinTestnet ? '1' : '3'}'/0'/0/0`); // 1' for testnet, 3' for mainnet
  const privateKeyWIF = child.toWIF();
  const keyPair = ECPair.fromWIF(privateKeyWIF, network);

  // Get public address
  const { address } = getLegacyP2PKH(Buffer.from(keyPair.publicKey));

  return {
    privateKey: privateKeyWIF,
    address,
    mnemonic,
  };
}

// P2PKH type with 34 chars
// Ex: nVaCZYVpHi1hVXqCUmJ6noogUKXwMkMC4z
export const getLegacyP2PKH = (publicKeyBuffer: Buffer) => {
  return dogecoin.payments.p2pkh({ pubkey: publicKeyBuffer, network: network });
};

export const initKeyPair = (WIF) => {
  if (!WIF) throw new Error('WALLET_PRIVATE_KEY is not set');
  const keyPair = ECPair.fromWIF(WIF, network);
  const signer: dogecoin.Signer = {
    publicKey: Buffer.from(keyPair.publicKey),
    sign: (hash: Buffer) => Buffer.from(keyPair.sign(hash)),
  };
  return {
    signer,
    publicKey: Buffer.from(keyPair.publicKey),
  };
};

export const transferDoge = async () => {
  const { signer } = initKeyPair(process.env.DOGE_PRIVATE_WIF);
  const { address: sender } = getLegacyP2PKH(signer.publicKey);
  console.log('Signer address:', sender);

  const to = sender;
  const amount = ONE_DOGE;

  const api = 'https://doge-electrs-testnet-demo.qed.me';

  interface IUTXO {
    txid: string;
    vout: number;
    value: number;
  }
  const getUTXO = async (address: string): Promise<IUTXO[]> => {
    const restURL = `${api}/address/${address}/utxo`;
    const res = await axios.get(restURL);
    return res.data;
  };
  const getTxHex = async (txId: string): Promise<string> => {
    const restURL = `${api}/tx/${txId}/hex`;
    const res = await axios.get(restURL);
    return res.data;
  };
  const broadcast = async (txHex: string) => {
    try {
      console.log('Broadcasting tx:', txHex);
      const res = await axios.post(`${api}/tx`, txHex);
      return res.data;
    } catch (e) {
      console.error('Broadcast tx error:', e.message, e.response?.data);
    }
  };

  const utxos = await getUTXO(sender);
  if (!utxos.length) throw new Error('No UTXO found');

  const totalUnspent = utxos.reduce((sum, { value }) => sum + value, 0);

  // Build transaction
  /**
   * @description Estimate fee for a transaction
   * For a simple P2PKH transaction:
   * - Input size: ~148 bytes per input
   * - Output size: ~34 bytes per output
   * - Overhead: ~10 bytes
   */
  const estimateFee = (numInputs, numOutputs, feeRate) => {
    const txSize = 10 + numInputs * 148 + numOutputs * 34;
    return txSize * feeRate;
  };

  const feeRate = 1_000; // sat/vB
  const maximumFeeRate = feeRate * 1.5;
  const psbt = new dogecoin.Psbt({ network, maximumFeeRate });
  const inputs: any[] = [];
  for (const utxo of utxos) {
    const input = { hash: utxo.txid, index: utxo.vout, sequence: 0xffffffff };
    const hex = await getTxHex(utxo.txid);
    input['nonWitnessUtxo'] = Buffer.from(hex, 'hex');
    inputs.push(input);
  }
  psbt.addInputs(inputs);
  psbt.addOutput({ address: to, value: amount });

  // Validate final output with fee
  const remainBal = totalUnspent - amount;
  const numInputs = utxos.length;
  const numOutputs = 2; // to + change
  const finalFee = estimateFee(numInputs, numOutputs, feeRate);
  console.info(`Inputs: ${numInputs}, Outputs: ${numOutputs}, FeeRate: ${feeRate}, Estimated Fee: ${finalFee}`);
  const DUST_THRESHOLD = ONE_DOGE;
  if (totalUnspent < finalFee + amount + DUST_THRESHOLD) {
    throw `Total less than fee + amount + DUST_THRESHOLD: ${totalUnspent} < ${finalFee} + ${amount} + ${DUST_THRESHOLD}`;
  }
  psbt.addOutput({ address: sender, value: remainBal - finalFee });

  // Sign transaction
  psbt.signAllInputs(signer);
  psbt.finalizeAllInputs();

  // Broadcast transaction
  const tx = psbt.extractTransaction();
  const txId = await broadcast(tx.toHex());
  txId && console.log(`Transaction successfully broadcasted! TxId is https://doge-testnet-explorer.qed.me/tx/${txId}`);
};

The comments record @dogiwallet/dogecoinjs-lib 0.1.4, a bip32 5 release candidate, bip39 3.1.0, ecpair 3.0.0, and tiny-secp256k1 2.2.3 ranges. This is source context, not a newly validated package combination.

Derivation and transfer have different boundaries

The wallet helper returns a WIF private key and mnemonic alongside the address. That object contains secrets even when it is convenient for a local experiment; it should not be treated as ordinary log or UI data. The transfer helper instead loads WIF from DOGE_PRIVATE_WIF. No key value appears in the article.

The network is explicitly Dogecoin testnet, and the example destination equals the sender. Public faucet/API links in code comments are historical references, not verified current services.

Estimate, sign, then inspect the actual result

The helper selects every discovered UTXO and estimates size from fixed P2PKH input/output constants. Its fee rate and one-DOGE dust threshold are example policy assumptions, not universal network rules. The final signed transaction and actual policy need separate checks.

The broadcast helper catches an error and returns no explicit failure object. A log or returned transaction ID also does not establish confirmation. No mnemonic, wallet, signature, network request, or transaction was generated for this revision. Source: Gist revision.

← Back to the garden