Skip to content

Instantly share code, notes, and snippets.

@david-dacruz
Created December 11, 2023 13:49
Show Gist options
  • Save david-dacruz/18d09798282dfd0a1129a92cc0d6df42 to your computer and use it in GitHub Desktop.
Save david-dacruz/18d09798282dfd0a1129a92cc0d6df42 to your computer and use it in GitHub Desktop.
DRC20 wallet creation
import { hostname } from 'os'
import { WALLET_PATH, Wallets } from './wallets.js'
export const create = (app) => app.command('walletcreate')
.description('Create one or more new wallet')
.option('-c, --count <count>', 'amount of wallet to create')
.option('-p, --prefix <prefix>', 'prefix to use whenever naming wallets', hostname())
.action(async ({ count, prefix }) => {
if (count === undefined) {
console.error('You must define a number of wallet to generate: eg --count=10')
return -1
}
console.log(`Creating ${count} new wallets`)
let wallets = new Wallets(WALLET_PATH)
await wallets.loadFromDisk()
if (!wallets.empty()) {
console.log(`${WALLET_PATH} not empty, wallets already present`)
return -1
}
console.log('Creating wallets')
wallets = await Wallets.create(prefix, count)
const fundingWallet = wallets.fundingWallet()
if (!fundingWallet) {
console.log('Wallets generation failed')
}
console.log(`Wallets generated, add funds to your funding wallet with ${'walletfund'}`)
})
import { readFile, writeFile } from 'node:fs/promises'
import { ulid } from 'ulid'
import { existsSync } from 'node:fs'
import dogecore from 'bitcore-lib-doge'
const { PrivateKey } = dogecore
export const WALLET_PATH = './wallets.json'
export class Wallet {
constructor (name, address, privkey, utxos = [], { isFunding } = {}) {
this.name = name
this.address = address
this.privkey = privkey
this.utxos = utxos
if (isFunding) {
this.isFunding = true
}
}
satBalance () {
return this.utxos.reduce((acc, x) => acc + x.satoshis, 0)
}
addUtxo (utxo) {
const duplicate = this.utxos.find((stored) => stored.txid === utxo.txid && stored.satoshis === utxo.satoshis)
if (duplicate !== undefined) {
if (duplicate.satoshis !== utxo.satoshis) {
if (utxo.satoshis < duplicate.satoshis) {
// fees
console.error(`local satoshi count mismatch txid=${utxo.txid} stored=${duplicate.satoshis} truth=${utxo.satoshis}, using chain utxo as truth`)
const index = this.utxos.findIndex((stored) => duplicate.txid === stored.txid)
this.utxos[index] = utxo
}
console.error(`local satoshi count mismatch! txid=${utxo.txid} stored=${duplicate.satoshis} truth=${utxo.satoshis}`)
} else {
console.log(`ignoring already present utxo: ${utxo.txid}`)
}
return
}
this.utxos.push(utxo)
}
/**
* @param {Transaction} tx
*/
addTx(tx) {
this.utxos = this.utxos.filter(utxo => {
for (const input of tx.inputs) {
// todo this is not valid JS, there is not toString/1, maybe Buffer.from(input.prevTxId).toString('hex') ?
if (input.prevTxId.toString('hex') === utxo.txid && input.outputIndex === utxo.vout) {
return false
}
}
return true
})
tx.outputs.forEach((output, vout) => {
if (output.script.toAddress().toString() === this.address) {
this.utxos.push({
txid: tx.hash,
vout,
script: output.script.toHex(),
satoshis: output.satoshis,
})
}
})
}
synchronizeUtxos (utxos = []) {
if (!Array.isArray(utxos)) {
throw new Error(`utxos must be an array of utxos`)
}
for (const utxo of utxos) {
this.addUtxo(utxo)
}
}
static create (prefix) {
const privateKey = new PrivateKey()
const wif = privateKey.toWIF()
const address = privateKey.toAddress().toString()
return new Wallet(`${prefix}-${ulid()}`, address, wif, [])
}
static createFunding (prefix) {
const privateKey = new PrivateKey()
const wif = privateKey.toWIF()
const address = privateKey.toAddress().toString()
return new Wallet(`${prefix}-funding-${ulid()}`, address, wif, [], { isFunding: true })
}
}
export class Wallets {
constructor (path) {
this.path = path
// all wallets
this._wallets = {}
// used for funding
this._fundingWallet = null
}
/**
* @param {Wallet} wallet
*/
update(wallet) {
this._wallets[wallet.name] = wallet
}
/**
* @param {Wallet} wallet
*/
add (wallet) {
if (this._wallets[wallet.name] !== undefined) {
console.error(`Wallet already added: ${wallet.name}`)
}
this._wallets[wallet.name] = wallet
}
/**
* @returns {null | Wallet}
*/
fundingWallet () {
if (this._fundingWallet) {
return this._fundingWallet
}
this._fundingWallet = Object.values(this._wallets)
.find((wallet) => Boolean(wallet.isFunding))
return this._fundingWallet
}
/**
* @return {Wallet[]}
*/
mintingWallets () {
return Object.values(this._wallets).filter((wallet) => !Boolean(wallet.isFunding))
}
async loadFromDisk () {
if (existsSync(this.path)) {
const content = await readFile(this.path, 'utf-8')
const rawWallet = JSON.parse(content)
this._wallets = Object.values(rawWallet).map(({
name,
privkey,
address,
utxos,
isFunding
}) => new Wallet(name, address, privkey, utxos, { isFunding }))
.reduce((acc, x) => {
acc[x.name] = x
return acc
}, {})
this._fundingWallet = this.fundingWallet()
}
}
async saveToDisk () {
const content = JSON.stringify(this._wallets, undefined, 2)
await writeFile(this.path, content, 'utf-8')
}
/**
* @param {string} prefix
* @param {number} count
* @returns {Wallets}
*/
static async create (prefix, count) {
const wallets = new Wallets(WALLET_PATH)
// generate count wallet
for (let i = 0; i < count; i++) {
const wallet = Wallet.create(prefix)
wallets.add(wallet)
}
// generate a funding wallet, this will be the input and output wallet
wallets.add(Wallet.createFunding(prefix))
await wallets.saveToDisk()
return wallets
}
static async load () {
const wallets = new Wallets(WALLET_PATH)
await wallets.loadFromDisk()
if (wallets.empty()) {
return null
}
return wallets
}
empty () {
return Object.keys(this._wallets).length === 0
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment