@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
522 lines (454 loc) • 18.9 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unused-vars */
import {
Beef,
CachedKeyDeriver,
CreateActionArgs,
InternalizeActionArgs,
KeyDeriver,
KeyDeriverApi,
PrivateKey,
Utils,
WalletInterface
} from '@bsv/sdk'
import {
Services,
asString,
StorageKnex,
sdk,
verifyOne,
verifyId,
ScriptTemplateBRC29,
randomBytesBase64,
randomBytes,
EntityProvenTxReq,
TableOutput
} from '../../../src/index.all'
import { _tu, TestWalletNoSetup, TestWalletOnly } from '../../utils/TestUtilsWalletStorage'
import dotenv from 'dotenv'
dotenv.config()
describe('walletLive test', () => {
jest.setTimeout(99999999)
const env = _tu.getEnv('test')
const myIdentityKey = env.identityKey
const myRootKeyHex = env.devKeys[myIdentityKey]
if (!myIdentityKey || !myRootKeyHex)
throw new sdk.WERR_INVALID_OPERATION(
`Requires a .env file with MY_${env.chain.toUpperCase()}_IDENTITY and corresponding DEV_KEYS entries.`
)
const myIdentityKey2 = env.identityKey2
if (!myIdentityKey2)
throw new sdk.WERR_INVALID_OPERATION(
`Requires a .env file with MY_${env.chain.toUpperCase()}_IDENTITY2 and corresponding DEV_KEYS entries.`
)
const myRootKeyHex2 = env.devKeys[myIdentityKey2!]
if (!myRootKeyHex2)
throw new sdk.WERR_INVALID_OPERATION(
`Requires a .env file with MY_${env.chain.toUpperCase()}_IDENTITY2 and corresponding DEV_KEYS entries.`
)
let myCtx: TestWalletOnly
let myCtx2: TestWalletOnly
const ctxs: TestWalletNoSetup[] = []
let stagingStorage: StorageKnex
beforeAll(async () => {
myCtx = await _tu.createTestWalletWithStorageClient({
rootKeyHex: myRootKeyHex,
chain: env.chain
})
myCtx2 = await _tu.createTestWalletWithStorageClient({
rootKeyHex: myRootKeyHex2,
chain: env.chain
})
const connection = JSON.parse(process.env.TEST_CLOUD_MYSQL_CONNECTION || '')
const knex = _tu.createMySQLFromConnection(connection)
stagingStorage = (
await _tu.createKnexTestWallet({
knex,
chain: 'test',
databaseName: 'staging_wallet_storage',
rootKeyHex: myRootKeyHex
})
).activeStorage
})
afterAll(async () => {
await stagingStorage.destroy()
await myCtx.storage.destroy()
for (const ctx of ctxs) await ctx.storage.destroy()
})
test('1 set change outputs spendable false if not valid utxos', async () => {
// Check the list of outputs first using the debugger breakpoint, before updating spendable flags.
for (const { wallet, activeStorage: storage, services } of ctxs) {
const { invalidSpendableOutputs: notUtxos } = await confirmSpendableOutputs(storage, services)
const outputsToUpdate = notUtxos.map(o => ({
id: o.outputId,
satoshis: o.satoshis
}))
const total: number = outputsToUpdate.reduce((t, o) => t + o.satoshis, 0)
debugger
// *** About set spendable = false for outputs ***/
for (const o of outputsToUpdate) {
await storage.updateOutput(o.id, { spendable: false })
}
}
})
test('2 review available change', async () => {
for (const { wallet, activeStorage: storage, services, userId } of ctxs) {
const { basketId } = verifyOne(
await storage.findOutputBaskets({
partial: { userId, name: 'default' }
})
)
const r: object = {}
for (const { name, txStatus } of [
{ name: 'completed', txStatus: <sdk.TransactionStatus[]>['completed'] },
{ name: 'nosend', txStatus: <sdk.TransactionStatus[]>['nosend'] },
{ name: 'unproven', txStatus: <sdk.TransactionStatus[]>['unproven'] },
{ name: 'failed', txStatus: <sdk.TransactionStatus[]>['failed'] },
{ name: 'sending', txStatus: <sdk.TransactionStatus[]>['sending'] },
{
name: 'unprocessed',
txStatus: <sdk.TransactionStatus[]>['unprocessed']
},
{ name: 'unsigned', txStatus: <sdk.TransactionStatus[]>['unsigned'] }
]) {
const or = {
txStatus,
outputs: await storage.findOutputs({
partial: { basketId, spendable: true },
txStatus
}),
outputCount: 0,
total: <number>0
}
or.total = or.outputs.reduce((t, o) => t + o.satoshis, 0)
or.outputCount = or.outputs.length
or.outputs = []
r[name] = or
}
expect(r).toBeTruthy()
let log = ''
for (const [k, v] of Object.entries(r)) {
if (v.outputCount > 0) log += `${k} count=${v.outputCount} total=${v.total}\n`
}
console.log(log)
}
})
test.skip('3 abort incomplete transactions', async () => {
for (const { wallet, activeStorage: storage, services, userId } of ctxs) {
const txs = await storage.findTransactions({
partial: { userId },
status: ['unsigned']
})
const total = txs.reduce((s, t) => s + t.satoshis, 0)
debugger
for (const tx of txs) {
await wallet.abortAction({ reference: tx.reference })
}
}
})
test.skip('4 create a wallet payment output', async () => {
const r = await createWalletPaymentAction({
toIdentityKey: '02bec52b12b8575f981cf38f3739ffbbfe4f6c6dbe4310d6384b6e97b122f0d087',
outputSatoshis: 100 * 1000,
keyDeriver: myCtx.keyDeriver,
wallet: myCtx.wallet,
logResult: true
})
})
test('5 pull out txid from BEEF', async () => {
const beefHex =
'010101015c574f48257202b9bff1b14baaa31cea24b9132555216900c277566d440250c50200beef01fee100190003020602f1fdbfa55c7227d2d9f93b7c2b83596a8e336ced483c1616dd98e8a32054dc6307010102009602f0b0959d085cbfda1a0958f65882b1a2829d66853582c0a530586dd00e930100002a7f27bd83b7d490f6641bbd3a8bdeff31490c14430597302ba579392be33f730201000100000001faba5977e9d7894778490ad3d3cf3ff0144da2920f6b31869dbcc026b693061b000000006a473044022050b2a300cad0e4b4c5ecaf93445937f21f6ec61d0c1726ac46bfb5bc2419af2102205d53e70fbdb0d1181a3cb1ef437ae27a73320367fdb78e8cadbfcbf82054e696412102480166f272ee9b639317c16ee60a2254ece67d0c7190fedbd26d57ac30f69d65ffffffff1da861000000000000c421029b09fdddfae493e309d3d97b919f6ab2902a789158f6f78489ad903b7a14baeaac2131546f446f44744b7265457a6248594b466a6d6f42756475466d53585855475a4735840423b7e26b5fd304a88f2ea28c9cf04d6c0a6c52a3174b69ea097039a355dbc6d95e702ac325c3f07518c9b4370796f90ad74e1c46304402206cd8228dd5102f7d8bd781e71dbf60df6559e90df9b79ed1c2b51d0316432f5502207a8713e899232190322dd4fdac6384f6b416ffa10b4196cdc7edbaf751b4a1156d7502000000000000001976a914ee8f77d351270123065a30a13e30394cbb4a6a2b88ace8030000000000001976a9147c8d0d51b07812872049e60e65a28d1041affc1f88ace8030000000000001976a914494c42ae91ebb8d4df662b0c2c98acfcbf14aff388ac93070000000000001976a9149619d3a2c3669335175d6fbd1d785719418cd69588acef030000000000001976a91435aabdafdc475012b7e2b7ab42e8f0fd9e8b665588ac59da0000000000001976a914c05b882ce290b3c19fbb0fca21e416f204d855a188acf3030000000000001976a9146ccff9f5e40844b784f1a68269afe30f5ec84c5d88accb340d00000000001976a914baf2186a8228a9581e0af744e28424343c6a464d88ace9030000000000001976a914a9c3b08f698df167c352f56aad483c907a0e64f488ac61140000000000001976a914f391b03543456ca68f3953b5ef4883f1299b4a2c88ac44c10500000000001976a914e6631bf6d96f93db48fb51daeace803ad805c09788ace9030000000000001976a9148cac2669fc696f5fb39aa59360a2cd20a6daffac88ac49b00400000000001976a9142c16b8a63604c66aa51f47f499024e327657ab5388acd7d50100000000001976a914ca5b56f03f796f55583c7cdd612c02f8d232669388ac42050000000000001976a914175a6812dbf2a550b1bf0d21594b96f9daf60d7988ac15040000000000001976a9147422a7237bb0fa77691047abf930e0274d193fe788ace9030000000000001976a9141a32c1c07dd4f9c632ce6b43dd28c8b27a37d81588ace8030000000000001976a914d9433de1883950578e9a29013aedb1e66d900bdc88ac39190000000000001976a9149fcdbc118b0114d2cc086a75eb14d880e3e25a9e88ac55390200000000001976a914cccf036ec7ae05690461c9d1be10193b6427055588ac1d010000000000001976a9148578396af7a6783824ff680315cc0a1375d9586e88acb3090000000000001976a9147c63cace8600f5400c8678cb2c843400c0c8ac2788acc55d0000000000001976a9148bf6991866b525f36dda54f7ca393c3a56cfff7188acc9100b00000000001976a914af41bf9bbf9d345f6b7cb37958d4cf43e88b17ef88acda040000000000001976a914ad818fcb671cc5b85dc22056d97a9b31aede4f3288ace8030000000000001976a91403ae9f7e41baee27ab7e66a323e73ee6801b5e1688ac59040000000000001976a9149f19356274a53ffdfb755bd81d40a97fe79b5e9b88ac10340000000000001976a914504dff507fccce4005f2d374cbdb6d5d493ceda288ac00000000000100000001f1fdbfa55c7227d2d9f93b7c2b83596a8e336ced483c1616dd98e8a32054dc63060000006b4830450221009bb61b5ec65cbcee0705cf757eba43e1716bfebf3ef976a09ffc926edee9ce6c022060606f5b5e59a6210067633a1263c23156d426feb1912cea480789beef62568741210208132e357b0d061848e779700eae5d69e5240a2503dd753a00e6cb3a8a920255ffffffff06e8030000000000001976a9149cedb88029c24f8bb9824628dfa0a023c1db5edc88acb40a0000000000001976a914da9b117c6880799eb3a0d0ccca252d7f11be240588ac35290000000000001976a914a8f814d3e2a2112bfe2f158f0596314a4379d45088ac54600000000000001976a91476f6f9a9ede3b7e496c921e6730be0af8d3fdfda88ac0e040000000000001976a91419a4615e24931e0e3b25e150fb362b56c5f4e89688ac253e0000000000001976a91435f0dcc5f8c47821a9d24d456b09995981cdb03f88ac00000000'
const beef = Beef.fromString(beefHex)
const btx = beef.findTxid('5c574f48257202b9bff1b14baaa31cea24b9132555216900c277566d440250c5')
console.log(`
tx: '${Utils.toHex(btx?.rawTx!)}'
${beef.toLogString()}
`)
})
test('5z send a wallet payment from myCtx to second wallet', async () => {
const tauriRootKey = '1363ef9b14531a52648e1e7e7f430a10ceda1df8d514a2a75d8404094f14a649'
const tauriIdentityKey = PrivateKey.fromHex(tauriRootKey).toPublicKey().toString()
const r = await createWalletPaymentAction({
toIdentityKey: tauriIdentityKey,
outputSatoshis: 1000 * 1000,
keyDeriver: myCtx.keyDeriver,
wallet: myCtx.wallet,
logResult: true
})
const toCtx = await _tu.createTestWalletWithStorageClient({
rootKeyHex: tauriRootKey,
chain: env.chain
})
const args: InternalizeActionArgs = {
tx: Utils.toArray(r.atomicBEEF, 'hex'),
outputs: [
{
outputIndex: r.vout,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: r.derivationPrefix,
derivationSuffix: r.derivationSuffix,
senderIdentityKey: r.senderIdentityKey
}
}
],
description: 'from tone wallet'
}
const rw = await toCtx.wallet.internalizeAction(args)
expect(rw.accepted).toBe(true)
})
test('6 send a wallet payment from myCtx to second wallet', async () => {
const r = await createWalletPaymentAction({
toIdentityKey: myIdentityKey2,
outputSatoshis: randomBytes(1)[0] + 10,
keyDeriver: myCtx.keyDeriver,
wallet: myCtx.wallet,
logResult: true
})
const toCtx = myCtx2
const args: InternalizeActionArgs = {
tx: Utils.toArray(r.atomicBEEF, 'hex'),
outputs: [
{
outputIndex: r.vout,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: r.derivationPrefix,
derivationSuffix: r.derivationSuffix,
senderIdentityKey: r.senderIdentityKey
}
}
],
description: 'from live wallet'
}
const rw = await toCtx.wallet.internalizeAction(args)
expect(rw.accepted).toBe(true)
const beef = Beef.fromString(r.atomicBEEF)
const btx = beef.txs.slice(-1)[0]
const txid = btx.txid
const req = await EntityProvenTxReq.fromStorageTxid(stagingStorage, txid)
expect(req?.notify.transactionIds?.length).toBe(2)
})
test('6a help setup my own wallet', async () => {
const privKey = PrivateKey.fromRandom()
const identityKey = privKey.toPublicKey().toString()
const log = `
// Add the following to .env file:
MY_TEST_IDENTITY = '${identityKey}'
DEV_KEYS = '{
"${identityKey}": "${privKey.toString()}"
}'
`
console.log(log)
})
test('6b run liveWallet Monitor once', async () => {
const liveCtx = ctxs[0]
await liveCtx.monitor.runOnce()
expect(1 === 1)
})
test('6c send a wallet payment from live to your own wallet', async () => {
const myIdentityKey = env.identityKey
const myRootKeyHex = env.devKeys[myIdentityKey]
if (!myIdentityKey || !myRootKeyHex)
throw new sdk.WERR_INVALID_OPERATION(
`Requires a .env file with MY_${env.chain.toUpperCase()}_IDENTITY and corresponding DEV_KEYS entries.`
)
const toIdentityKey: string = '02947542cf31c8d91c303bba8f981ee9595c414e63c185d495a97c558aa7b2e522'
const r = createWalletPaymentOutput({
toIdentityKey,
fromRootKeyHex: myRootKeyHex,
logResult: true
})
console.log(`\n${JSON.stringify(r)}\n`)
})
test('6d make atomicBEEF for known txid', async () => {
const txid = '6b9e8ed767ed6e6366527ddf8707637f3aaee1093085985c1dd04f347a3c25be'
const beef = new Beef()
beef.mergeTxidOnly(txid)
console.log(`
BEEF for known txid ${txid}
${Utils.toHex(beef.toBinaryAtomic(txid))}
`)
})
test('6e make atomicBEEF for txid from staging-dojo', async () => {
const myIdentityKey = env.identityKey
const myRootKeyHex = env.devKeys[myIdentityKey]
const txid = '6b9e8ed767ed6e6366527ddf8707637f3aaee1093085985c1dd04f347a3c25be'
const beef = await stagingStorage.getBeefForTransaction(txid, {})
console.log(`
${beef.toLogString()}
AtomicBEEF for known txid ${txid}
${Utils.toHex(beef.toBinaryAtomic(txid))}
`)
})
test('7 test two client wallets', async () => {
if (!myIdentityKey2) return
if (myCtx) await myCtx.storage.destroy()
myCtx = await _tu.createTestWalletWithStorageClient({
rootKeyHex: myRootKeyHex,
chain: env.chain
})
{
const u1 = await myCtx.storage.findOrInsertUser(myIdentityKey)
}
if (myCtx) await myCtx.storage.destroy()
if (myCtx2) await myCtx2.storage.destroy()
myCtx2 = await _tu.createTestWalletWithStorageClient({
rootKeyHex: myRootKeyHex2,
chain: env.chain
})
{
const u2 = await myCtx2.storage.findOrInsertUser(myIdentityKey2)
}
if (myCtx) await myCtx.storage.destroy()
if (myCtx2) await myCtx2.storage.destroy()
myCtx = await _tu.createTestWalletWithStorageClient({
rootKeyHex: myRootKeyHex,
chain: env.chain
})
myCtx2 = await _tu.createTestWalletWithStorageClient({
rootKeyHex: myRootKeyHex2,
chain: env.chain
})
{
const u1 = await myCtx.storage.findOrInsertUser(myIdentityKey)
const u2 = await myCtx2.storage.findOrInsertUser(myIdentityKey2)
expect(u1.user.userId).not.toBe(u2.user.userId)
}
})
// End of describe
})
async function confirmSpendableOutputs(
storage: StorageKnex,
services: Services
): Promise<{ invalidSpendableOutputs: TableOutput[] }> {
const invalidSpendableOutputs: TableOutput[] = []
const users = await storage.findUsers({ partial: {} })
for (const { userId } of users) {
const defaultBasket = verifyOne(await storage.findOutputBaskets({ partial: { userId, name: 'default' } }))
const where: Partial<TableOutput> = {
userId,
basketId: defaultBasket.basketId,
spendable: true
}
const outputs = await storage.findOutputs({ partial: where })
for (let i = outputs.length - 1; i >= 0; i--) {
const o = outputs[i]
const oid = verifyId(o.outputId)
if (o.spendable) {
let ok = false
if (o.lockingScript && o.lockingScript.length > 0) {
const r = await services.getUtxoStatus(asString(o.lockingScript), 'script')
if (r.status === 'success' && r.isUtxo && r.details?.length > 0) {
const tx = await storage.findTransactionById(o.transactionId)
if (
tx &&
tx.txid &&
r.details.some(d => d.txid === tx.txid && d.satoshis === o.satoshis && d.index === o.vout)
) {
ok = true
}
}
}
if (!ok) {
invalidSpendableOutputs.push(o)
}
}
}
}
return { invalidSpendableOutputs }
}
export function createWalletPaymentOutput(args: {
toIdentityKey: string
fromRootKeyHex: string
logResult?: boolean
}): {
senderIdentityKey: string
derivationPrefix: string
derivationSuffix: string
lockingScript: string
} {
const t = new ScriptTemplateBRC29({
derivationPrefix: randomBytesBase64(8),
derivationSuffix: randomBytesBase64(8),
keyDeriver: new CachedKeyDeriver(PrivateKey.fromString(args.fromRootKeyHex))
})
const lockingScript = t.lock(args.fromRootKeyHex, args.toIdentityKey)
const r = {
senderIdentityKey: t.params.keyDeriver.identityKey,
derivationPrefix: t.params.derivationPrefix!,
derivationSuffix: t.params.derivationSuffix!,
lockingScript: lockingScript.toHex()
}
if (args.logResult) {
console.log(`
// createWalletPaymentOutput
const r = {
senderIdentityKey: '${r.senderIdentityKey}',
derivationPrefix: '${r.derivationPrefix}',
derivationSuffix: '${r.derivationSuffix}'
lockingScript: '${r.lockingScript}'
}`)
}
return r
}
export async function createWalletPaymentAction(args: {
toIdentityKey: string
outputSatoshis: number
keyDeriver: KeyDeriverApi
wallet: WalletInterface
logResult?: boolean
}): Promise<{
senderIdentityKey: string
vout: number
txid: string
derivationPrefix: string
derivationSuffix: string
atomicBEEF: string
}> {
const { toIdentityKey, outputSatoshis, keyDeriver, wallet } = args
const t = new ScriptTemplateBRC29({
derivationPrefix: randomBytesBase64(8),
derivationSuffix: randomBytesBase64(8),
keyDeriver
})
const createArgs: CreateActionArgs = {
description: `pay ${args.toIdentityKey}`.slice(0, 50),
labels: ['wallet-payment'],
outputs: [
{
satoshis: outputSatoshis,
lockingScript: t.lock(keyDeriver.rootKey.toString(), toIdentityKey).toHex(),
outputDescription: `for ${args.toIdentityKey}`.slice(0, 50),
basket: 'wallet-paymnet',
tags: ['wp-out']
}
],
options: {
//acceptDelayedBroadcast: false,
randomizeOutputs: false,
signAndProcess: true
}
}
const cr = await wallet.createAction(createArgs)
const r = {
senderIdentityKey: keyDeriver.identityKey,
vout: 0,
txid: cr.txid!,
derivationPrefix: t.params.derivationPrefix!,
derivationSuffix: t.params.derivationSuffix!,
atomicBEEF: Utils.toHex(cr.tx!)
}
if (args.logResult) {
console.log(`
// createWalletPaymentAction
const r = {
senderIdentityKey: '${r.senderIdentityKey}',
vout: 0,
txid: '${r.txid}',
derivationPrefix: '${r.derivationPrefix}',
derivationSuffix: '${r.derivationSuffix}'
atomicBEEF: '${r.atomicBEEF}'
}`)
}
return r
}