Created
December 11, 2023 13:49
-
-
Save david-dacruz/18d09798282dfd0a1129a92cc0d6df42 to your computer and use it in GitHub Desktop.
DRC20 wallet creation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'}`) | |
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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