UNPKG

@bsv/wallet-toolbox

Version:

BRC100 conforming wallet, wallet storage and wallet signer components

364 lines (333 loc) 10.9 kB
import { Beef, CreateActionArgs, CreateActionOptions, CreateActionResult, OutpointString, P2PKH, PublicKey, Script, SignActionArgs, SignActionOptions, SignActionResult } from '@bsv/sdk' import { EntityProvenTxReq, ScriptTemplateBRC29, sdk, Services, Setup, StorageKnex, verifyOne, verifyTruthy, wait } from '../../src' import { _tu, logger, TestWalletNoSetup, TuEnv } from './TestUtilsWalletStorage' import { validateCreateActionArgs, ValidCreateActionArgs } from '../../src/sdk' import { setDisableDoubleSpendCheckForTest } from '../../src/storage/methods/createAction' export interface LocalWalletTestOptions { setActiveClient: boolean useMySQLConnectionForClient: boolean useTestIdentityKey: boolean useIdentityKey2: boolean } export interface LocalTestWalletSetup extends TestWalletNoSetup { setActiveClient: boolean useMySQLConnectionForClient: boolean useTestIdentityKey: boolean useIdentityKey2: boolean } export async function createSetup(chain: sdk.Chain, options: LocalWalletTestOptions): Promise<LocalTestWalletSetup> { const env = _tu.getEnv(chain) let identityKey: string | undefined let filePath: string | undefined if (options.useTestIdentityKey) { identityKey = env.testIdentityKey filePath = env.testFilePath } else { if (options.useIdentityKey2) { identityKey = env.identityKey2 } else { identityKey = env.identityKey filePath = env.filePath } } if (!identityKey) throw new sdk.WERR_INVALID_PARAMETER('identityKey', 'valid') if (!filePath) filePath = `./backup-${chain}-${identityKey}.sqlite` const setup = { ...options, ...(await _tu.createTestWallet({ chain, rootKeyHex: env.devKeys[identityKey], filePath, setActiveClient: options.setActiveClient, addLocalBackup: false, useMySQLConnectionForClient: options.useMySQLConnectionForClient })) } console.log(`ACTIVE STORAGE: ${setup.storage.getActiveStoreName()}`) return setup } export async function burnOneSatTestOutput( setup: LocalTestWalletSetup, options: CreateActionOptions = {}, howMany: number = 1 ): Promise<void> { const outputs = await setup.wallet.listOutputs({ basket: 'test-output', include: 'entire transactions', limit: 1000 }) while (howMany-- > 0) { const o = outputs.outputs.find(o => o.satoshis === 1) if (!o) break console.log(`burning ${o.outpoint}`) const inputBEEF = outputs.BEEF const p2pkh = new P2PKH() const args: CreateActionArgs = { inputBEEF, inputs: [ { unlockingScriptLength: 108, outpoint: o.outpoint, inputDescription: 'burn 1 sat output' } ], description: 'burn output' } const bcar = await setup.wallet.createAction(args) expect(bcar.signableTransaction) const st = bcar.signableTransaction! const beef = Beef.fromBinary(st.tx) const tx = beef.findAtomicTransaction(beef.txs.slice(-1)[0].txid)! const unlock = p2pkh.unlock(setup.keyDeriver.rootKey, 'all', false) const unlockingScript = (await unlock.sign(tx, 0)).toHex() const signArgs: SignActionArgs = { reference: st.reference, spends: { 0: { unlockingScript } } } const sar = await setup.wallet.signAction(signArgs) console.log(sar.txid) expect(sar.txid) } } export async function createOneSatTestOutput( setup: LocalTestWalletSetup, options: CreateActionOptions = {}, howMany: number = 1 ): Promise<CreateActionResult> { if (howMany < 1) throw new sdk.WERR_INVALID_PARAMETER('howMany', 'at least 1') let car: CreateActionResult = {} let noSendChange: OutpointString[] | undefined = undefined let txids: string[] = [] let vargs: ValidCreateActionArgs for (let i = 0; i < howMany; i++) { const args: CreateActionArgs = { outputs: [ { lockingScript: new P2PKH().lock(PublicKey.fromString(setup.identityKey).toAddress()).toHex(), satoshis: 1, outputDescription: 'test output', customInstructions: JSON.stringify({ type: 'P2PKH', key: 'identity' }), basket: 'test-output' } ], description: 'create test output', options: { ...options, noSendChange } } vargs = validateCreateActionArgs(args) car = await setup.wallet.createAction(args) expect(car.txid) txids.push(car.txid!) noSendChange = car.noSendChange const req = await EntityProvenTxReq.fromStorageTxid(setup.activeStorage, car.txid!) expect(req !== undefined && req.history.notes !== undefined) if (req && req.history.notes) { if (vargs.isNoSend) { expect(req.status === 'nosend').toBe(true) expect(req.history.notes.length).toBe(1) const n = req.history.notes[0] expect(n.what === 'status' && n.status_now === 'nosend').toBe(true) } else { expect(req.status === 'unsent').toBe(true) expect(req.history.notes.length).toBe(1) const n = req.history.notes[0] expect(n.what === 'status' && n.status_now === 'unsent').toBe(true) } } } if (vargs!.isNoSend) { // Create final sending transaction const args: CreateActionArgs = { description: 'send batch', options: { ...options, sendWith: txids } } vargs = validateCreateActionArgs(args) car = await setup.wallet.createAction(args) } return car } export async function recoverOneSatTestOutputs(setup: LocalTestWalletSetup, testOptionsMode?: 1): Promise<void> { const outputs = await setup.wallet.listOutputs({ basket: 'test-output', include: 'entire transactions', limit: 1000 }) if (outputs.outputs.length > 0) { const args: CreateActionArgs = { inputBEEF: outputs.BEEF!, inputs: [], description: 'recover test output' } if (testOptionsMode === 1) { args.options = { acceptDelayedBroadcast: false } } const p2pkh = new P2PKH() for (const o of outputs.outputs) { args.inputs!.push({ unlockingScriptLength: 108, outpoint: o.outpoint, inputDescription: 'recovered test output' }) } const car = await setup.wallet.createAction(args) expect(car.signableTransaction) const st = car.signableTransaction! const beef = Beef.fromBinary(st.tx) const tx = beef.findAtomicTransaction(beef.txs.slice(-1)[0].txid)! const signArgs: SignActionArgs = { reference: st.reference, spends: {} // 0: { unlockingScript } }, } for (let i = 0; i < outputs.outputs.length; i++) { const o = outputs.outputs[i] const unlock = p2pkh.unlock(setup.keyDeriver.rootKey, 'all', false) const unlockingScript = (await unlock.sign(tx, i)).toHex() signArgs.spends[i] = { unlockingScript } } const sar = await setup.wallet.signAction(signArgs) expect(sar.txid) } } export async function trackReqByTxid(setup: LocalTestWalletSetup, txid: string): Promise<void> { const req = await EntityProvenTxReq.fromStorageTxid(setup.activeStorage, txid) expect(req !== undefined && req.history.notes !== undefined) if (!req || !req.history.notes) throw new sdk.WERR_INTERNAL() let newBlocks = 0 let lastHeight: number | undefined for (; req.status !== 'completed'; ) { let height = setup.monitor.lastNewHeader?.height if (req.status === 'unsent') { // send it... } if (req.status === 'sending') { // send it... } if (req.status === 'unmined') { if (height && lastHeight) { if (height === lastHeight) { await wait(1000 * 60) } else { newBlocks++ expect(newBlocks < 5) } } } await setup.monitor.runOnce() await req.refreshFromStorage(setup.activeStorage) lastHeight = height } } /** * This method will normally throw an error on the initial createAction call due to the output being doublespent * @param setup * @param options * @returns */ export async function doubleSpendOldChange( setup: LocalTestWalletSetup, options: SignActionOptions ): Promise<SignActionResult> { const auth = await setup.wallet.storage.getAuth(true) if (!auth.userId || !setup.wallet.storage.getActive().isStorageProvider) throw new Error('active must be StorageProvider') const s = setup.wallet.storage.getActive() as StorageKnex const o = verifyOne( await s.findOutputs({ partial: { userId: auth.userId!, spendable: false, change: true }, paged: { limit: 1 } }) ) await s.validateOutputScript(o) const lockingScript = Script.fromBinary(o.lockingScript!) const otx = verifyTruthy(await s.findTransactionById(o.transactionId)) if (otx.status !== 'completed') throw new Error('output must be from completed transaction') const inputBEEF = (await s.getBeefForTransaction(o.txid!, {})).toBinary() logger(`spending ${o.txid} vout ${o.vout}`) const sabppp = new ScriptTemplateBRC29({ derivationPrefix: o.derivationPrefix, derivationSuffix: o.derivationSuffix, keyDeriver: setup.wallet.keyDeriver }) const args: CreateActionArgs = { inputBEEF, inputs: [ { unlockingScriptLength: 108, outpoint: `${o.txid!}.${o.vout}`, inputDescription: 'spent change output' } ], description: 'intentional doublespend', options } let car: CreateActionResult try { setDisableDoubleSpendCheckForTest(true) car = await setup.wallet.createAction(args) } finally { setDisableDoubleSpendCheckForTest(false) } expect(car.signableTransaction) const st = car.signableTransaction! const beef = Beef.fromBinary(st.tx) const tx = beef.findAtomicTransaction(beef.txs.slice(-1)[0].txid)! const unlock = sabppp.unlock(setup.rootKey.toHex(), setup.identityKey, o.satoshis, lockingScript) const unlockingScript = (await unlock.sign(tx, 0)).toHex() const signArgs: SignActionArgs = { reference: st.reference, spends: { '0': { unlockingScript } }, options } const sar = await setup.wallet.signAction(signArgs) return sar } export async function createMainReviewSetup(): Promise<{ env: TuEnv storage: StorageKnex services: Services }> { const env = _tu.getEnv('main') const knex = Setup.createMySQLKnex(process.env.MAIN_CLOUD_MYSQL_CONNECTION!) const storage = new StorageKnex({ chain: env.chain, knex: knex, commissionSatoshis: 0, commissionPubKeyHex: undefined, feeModel: { model: 'sat/kb', value: 1 } }) const servicesOptions = Services.createDefaultOptions(env.chain) if (env.whatsonchainApiKey) servicesOptions.whatsOnChainApiKey = env.whatsonchainApiKey const services = new Services(servicesOptions) storage.setServices(services) await storage.makeAvailable() return { env, storage, services } }