@bsv/wallet-toolbox
Version:
BRC100 conforming wallet, wallet storage and wallet signer components
678 lines (610 loc) • 23.8 kB
text/typescript
import { Beef, CreateActionArgs, InternalizeActionArgs, P2PKH, WalletProtocol } from '@bsv/sdk'
import { sdk } from '../../../src/index.all'
import { _tu, expectToThrowWERR, TestWalletNoSetup } from '../../utils/TestUtilsWalletStorage'
describe('internalizeAction tests', () => {
jest.setTimeout(99999999)
const env = _tu.getEnvFlags('test')
const gctxs: TestWalletNoSetup[] = []
const useSharedCtxs = true
beforeAll(async () => {
if (env.runMySQL) gctxs.push(await _tu.createLegacyWalletMySQLCopy('actionInternalizeActionTests'))
gctxs.push(await _tu.createLegacyWalletSQLiteCopy('actionInternalizeActionTests'))
})
afterAll(async () => {
for (const ctx of gctxs) {
await ctx.storage.destroy()
}
})
test('0 invalid params', async () => {
for (const { wallet } of gctxs) {
const beef0 = new Beef()
const beef1 = new Beef()
beef1.mergeTxidOnly('1'.repeat(64))
const invalidArgs: InternalizeActionArgs[] = [
{ tx: [], outputs: [], description: '' },
{ tx: [], outputs: [], description: '12345' },
{ tx: beef0.toBinary(), outputs: [], description: '12345' },
{ tx: beef1.toBinary(), outputs: [], description: '12345' }
// Oh so many things to test...
]
for (const args of invalidArgs) {
await expectToThrowWERR(sdk.WERR_INVALID_PARAMETER, () => wallet.internalizeAction(args))
}
}
})
test('1_internalize custom output in receiving wallet with checks', async () => {
const ctxs: TestWalletNoSetup[] = []
if (useSharedCtxs) ctxs.push(...gctxs)
else {
if (env.runMySQL) ctxs.push(await _tu.createLegacyWalletMySQLCopy('actionInternalizeAction1Tests'))
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('actionInternalizeAction1Tests'))
}
for (const { wallet } of ctxs) {
const root = '02135476'
const kp = _tu.getKeyPair(root.repeat(8))
const fredsAddress = kp.address
const outputSatoshis = 4
{
const createArgs: CreateActionArgs = {
description: `${kp.address} of ${root}`,
outputs: [
{
satoshis: outputSatoshis,
lockingScript: _tu.getLockP2PKH(fredsAddress).toHex(),
outputDescription: 'pay fred'
}
],
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
signAndProcess: true,
noSend: true
}
}
// This createAction creates a new P2PKH output of 4 satoshis for Fred using his publish payment address... old school.
const cr = await wallet.createAction(createArgs)
expect(cr.tx).toBeTruthy()
// Fred's new wallet (context)
const fred = await _tu.createSQLiteTestWallet({
chain: 'test',
databaseName: 'internalizeAction1fred',
rootKeyHex: '2'.repeat(64),
dropAll: true
})
// Internalize args to add fred's new output to his own wallet
const internalizeArgs: InternalizeActionArgs = {
tx: cr.tx!,
outputs: [
{
outputIndex: 0,
protocol: 'basket insertion',
insertionRemittance: {
basket: 'payments',
customInstructions: JSON.stringify({ root, repeat: 8 }),
tags: ['test', 'again']
}
}
],
description: 'got paid!'
}
// And do it...
const ir = await fred.wallet.internalizeAction(internalizeArgs)
expect(ir.accepted).toBe(true)
const ro = await fred.activeStorage.findOutputs({
partial: { outputId: 1 }
})
expect(ro[0].basketId).toBe(2) // Basket can't be default basket so basketId must be 2
expect(ro[0].satoshis).toBe(outputSatoshis)
// Validate custom instructions and tags
expect(ro[0].customInstructions).toBe(JSON.stringify({ root, repeat: 8 }))
const rtm = await fred.activeStorage.findOutputTagMaps({
partial: { outputId: 1 }
})
const rt1 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[0].outputTagId }
})
expect(rt1[0].tag).toBe('test')
const rt2 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[1].outputTagId }
})
expect(rt2[0].tag).toBe('again')
// Check that calling again does not throw an error
const r = await fred.wallet.internalizeAction(internalizeArgs)
await expect(Promise.resolve(r)).resolves.toBeTruthy()
// Cleanup Fred's storage
await fred.activeStorage.destroy()
}
}
if (!useSharedCtxs) {
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
}
})
test('2_internalize 2 custom outputs in receiving wallet with checks', async () => {
const ctxs: TestWalletNoSetup[] = []
if (useSharedCtxs) ctxs.push(...gctxs)
else {
if (env.runMySQL) ctxs.push(await _tu.createLegacyWalletMySQLCopy('actionInternalizeAction2Tests'))
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('actionInternalizeAction2Tests'))
}
for (const { wallet } of ctxs) {
const root = '02135476'
const kp = _tu.getKeyPair(root.repeat(8))
const fredsAddress = kp.address
const outputSatoshis1 = 4
const outputSatoshis2 = 5
{
const createArgs: CreateActionArgs = {
description: `${kp.address} of ${root}`,
outputs: [
{
satoshis: outputSatoshis1,
lockingScript: _tu.getLockP2PKH(fredsAddress).toHex(),
outputDescription: 'pay fred 1st payment'
},
{
satoshis: outputSatoshis2,
lockingScript: _tu.getLockP2PKH(fredsAddress).toHex(),
outputDescription: 'pay fred 2nd payment'
}
],
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
signAndProcess: true,
noSend: true
}
}
// This createAction creates a new P2PKH output of 4 and 5 satoshis for Fred using his publish payment address... old school.
const cr = await wallet.createAction(createArgs)
expect(cr.tx).toBeTruthy()
// Fred's new wallet (context)
const fred = await _tu.createSQLiteTestWallet({
chain: 'test',
databaseName: 'internalizeAction2fred',
rootKeyHex: '2'.repeat(64),
dropAll: true
})
// Internalize args to add fred's new output to his own wallet
const internalizeArgs: InternalizeActionArgs = {
tx: cr.tx!,
outputs: [
{
outputIndex: 0,
protocol: 'basket insertion',
insertionRemittance: {
basket: 'payments',
customInstructions: JSON.stringify({ root, repeat: 8 }),
tags: ['2 tests', 'test 1']
}
},
{
outputIndex: 1,
protocol: 'basket insertion',
insertionRemittance: {
basket: 'payments',
customInstructions: JSON.stringify({ root, repeat: 8 }),
tags: ['2 tests', 'test 2']
}
}
],
description: 'got paid twice!'
}
// And do it...
const ir = await fred.wallet.internalizeAction(internalizeArgs)
expect(ir.accepted).toBe(true)
{
const ro = await fred.activeStorage.findOutputs({
partial: { outputId: 1 }
})
expect(ro[0].basketId).toBe(2)
expect(ro[0].satoshis).toBe(outputSatoshis1)
// Validate custom instructions and tags
expect(ro[0].customInstructions).toBe(JSON.stringify({ root, repeat: 8 }))
const rtm = await fred.activeStorage.findOutputTagMaps({
partial: { outputId: 1 }
})
const rt1 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[0].outputTagId }
})
expect(rt1[0].tag).toBe('2 tests')
const rt2 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[1].outputTagId }
})
expect(rt2[0].tag).toBe('test 1')
}
{
const ro = await fred.activeStorage.findOutputs({
partial: { outputId: 2 }
})
expect(ro[0].basketId).toBe(2)
expect(ro[0].satoshis).toBe(outputSatoshis2)
expect(ro[0].customInstructions).toBe(JSON.stringify({ root, repeat: 8 }))
const rtm = await fred.activeStorage.findOutputTagMaps({
partial: { outputId: 2 }
})
const rt1 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[0].outputTagId }
})
expect(rt1[0].tag).toBe('2 tests')
const rt2 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[1].outputTagId }
})
expect(rt2[0].tag).toBe('test 2')
}
// Check that calling again does not throw an error
const r = await fred.wallet.internalizeAction(internalizeArgs)
await expect(Promise.resolve(r)).resolves.toBeTruthy()
await fred.activeStorage.destroy()
}
}
if (!useSharedCtxs) {
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
}
})
test('3_internalize wallet payment in receiving wallet with checks', async () => {
const ctxs: TestWalletNoSetup[] = []
if (useSharedCtxs) ctxs.push(...gctxs)
else {
if (env.runMySQL) ctxs.push(await _tu.createLegacyWalletMySQLCopy('actionInternalizeAction3Tests'))
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('actionInternalizeAction3Tests'))
}
for (const { wallet, identityKey: senderIdentityKey } of ctxs) {
const fred = await _tu.createSQLiteTestWallet({
chain: 'test',
databaseName: 'internalizeAction3fred',
rootKeyHex: '2'.repeat(64),
dropAll: true
})
const outputSatoshis = 5
const derivationPrefix = Buffer.from('invoice-12345').toString('base64')
const derivationSuffix = Buffer.from('utxo-0').toString('base64')
const brc29ProtocolID: WalletProtocol = [2, '3241645161d8']
const derivedPublicKey = wallet.keyDeriver.derivePublicKey(
brc29ProtocolID,
`${derivationPrefix} ${derivationSuffix}`,
fred.identityKey
)
const derivedAddress = derivedPublicKey.toAddress()
{
const createArgs: CreateActionArgs = {
description: `description BRC-29`,
outputs: [
{
satoshis: outputSatoshis,
lockingScript: new P2PKH().lock(derivedAddress).toHex(),
outputDescription: 'pay fred BRC-29'
}
],
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
signAndProcess: true,
noSend: true
}
}
const cr = await wallet.createAction(createArgs)
expect(cr.tx).toBeTruthy()
const internalizeArgs: InternalizeActionArgs = {
tx: cr.tx!,
outputs: [
{
outputIndex: 0,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: derivationPrefix,
derivationSuffix: derivationSuffix,
senderIdentityKey: senderIdentityKey
}
}
],
description: 'received BRC-29 payment!'
}
const ir = await fred.wallet.internalizeAction(internalizeArgs)
expect(ir.accepted).toBe(true)
const rfbs = await fred.activeStorage.findOutputBaskets({
partial: { name: 'default' }
})
expect(rfbs.length).toBe(1)
const rfos = await fred.activeStorage.findOutputs({
partial: { basketId: rfbs[0].basketId }
})
expect(rfos.length).toBe(1)
expect(rfos[0].satoshis).toBe(outputSatoshis)
expect(rfos[0].type).toBe('P2PKH')
expect(rfos[0].purpose).toBe('change')
const r = await fred.wallet.internalizeAction(internalizeArgs)
await expect(Promise.resolve(r)).resolves.toBeTruthy()
await fred.activeStorage.destroy()
}
}
if (!useSharedCtxs) {
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
}
})
test('4_internalize 2 wallet payments in receiving wallet with checks', async () => {
const ctxs: TestWalletNoSetup[] = []
if (useSharedCtxs) ctxs.push(...gctxs)
else {
if (env.runMySQL) ctxs.push(await _tu.createLegacyWalletMySQLCopy('actionInternalizeAction4Tests'))
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('actionInternalizeAction4Tests'))
}
for (const { wallet, identityKey: senderIdentityKey } of ctxs) {
const fred = await _tu.createSQLiteTestWallet({
chain: 'test',
databaseName: 'internalizeAction4fred',
rootKeyHex: '2'.repeat(64),
dropAll: true
})
const brc29ProtocolID: WalletProtocol = [2, '3241645161d8']
const outputSatoshis1 = 6
const derivationPrefix = Buffer.from('invoice-12345').toString('base64')
const derivationSuffix1 = Buffer.from('utxo-1').toString('base64')
const derivedPublicKey1 = wallet.keyDeriver.derivePublicKey(
brc29ProtocolID,
`${derivationPrefix} ${derivationSuffix1}`,
fred.identityKey
)
const derivedAddress1 = derivedPublicKey1.toAddress()
const outputSatoshis2 = 7
const derivationSuffix2 = Buffer.from('utxo-2').toString('base64')
const derivedPublicKey2 = wallet.keyDeriver.derivePublicKey(
brc29ProtocolID,
`${derivationPrefix} ${derivationSuffix2}`,
fred.identityKey
)
const derivedAddress2 = derivedPublicKey2.toAddress()
{
const createArgs: CreateActionArgs = {
description: `BRC-29 payments from other wallet`,
outputs: [
{
satoshis: outputSatoshis1,
lockingScript: new P2PKH().lock(derivedAddress1).toHex(),
outputDescription: 'pay fred 1st BRC-29 payment'
},
{
satoshis: outputSatoshis2,
lockingScript: new P2PKH().lock(derivedAddress2).toHex(),
outputDescription: 'pay fred 2nd BRC-29 payment'
}
],
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
signAndProcess: true,
noSend: true
}
}
const cr = await wallet.createAction(createArgs)
expect(cr.tx).toBeTruthy()
const internalizeArgs: InternalizeActionArgs = {
tx: cr.tx!,
outputs: [
{
outputIndex: 0,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: derivationPrefix,
derivationSuffix: derivationSuffix1,
senderIdentityKey: senderIdentityKey
}
},
{
outputIndex: 1,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: derivationPrefix,
derivationSuffix: derivationSuffix2,
senderIdentityKey: senderIdentityKey
}
}
],
description: 'received pair of BRC-29 payments!'
}
const ir = await fred.wallet.internalizeAction(internalizeArgs)
expect(ir.accepted).toBe(true)
const rfbs = await fred.activeStorage.findOutputBaskets({
partial: { name: 'default' }
})
expect(rfbs.length).toBe(1)
const rfos = await fred.activeStorage.findOutputs({
partial: { basketId: rfbs[0].basketId }
})
expect(rfos.length).toBe(2)
expect(rfos[0].satoshis).toBe(outputSatoshis1)
expect(rfos[0].type).toBe('P2PKH')
expect(rfos[0].purpose).toBe('change')
expect(rfos[1].satoshis).toBe(outputSatoshis2)
expect(rfos[1].type).toBe('P2PKH')
expect(rfos[1].purpose).toBe('change')
const r = await fred.wallet.internalizeAction(internalizeArgs)
await expect(Promise.resolve(r)).resolves.toBeTruthy()
await fred.activeStorage.destroy()
}
}
if (!useSharedCtxs) {
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
}
})
test('5_internalize 2 wallet payments and 2 basket insertions in receiving wallet with checks', async () => {
const ctxs: TestWalletNoSetup[] = []
if (env.runMySQL) ctxs.push(await _tu.createLegacyWalletMySQLCopy('actionInternalizeAction5Tests'))
ctxs.push(await _tu.createLegacyWalletSQLiteCopy('actionInternalizeAction5Tests'))
for (const { wallet, identityKey: senderIdentityKey } of ctxs) {
const fred = await _tu.createSQLiteTestWallet({
chain: 'test',
databaseName: 'internalizeAction5fred',
rootKeyHex: '2'.repeat(64),
dropAll: true
})
const brc29ProtocolID: WalletProtocol = [2, '3241645161d8']
const outputSatoshis1 = 8
const derivationPrefix = Buffer.from('invoice-12345').toString('base64')
const derivationSuffix1 = Buffer.from('utxo-1').toString('base64')
const derivedPublicKey1 = wallet.keyDeriver.derivePublicKey(
brc29ProtocolID,
`${derivationPrefix} ${derivationSuffix1}`,
fred.identityKey
)
const derivedAddress1 = derivedPublicKey1.toAddress()
const outputSatoshis2 = 9
const derivationSuffix2 = Buffer.from('utxo-2').toString('base64')
const derivedPublicKey2 = wallet.keyDeriver.derivePublicKey(
brc29ProtocolID,
`${derivationPrefix} ${derivationSuffix2}`,
fred.identityKey
)
const derivedAddress2 = derivedPublicKey2.toAddress()
const root = '02135476'
const kp = _tu.getKeyPair(root.repeat(8))
const fredsAddress = kp.address
const outputSatoshis3 = 10
const outputSatoshis4 = 11
{
const createArgs: CreateActionArgs = {
description: `BRC-29 payments from other wallet`,
outputs: [
{
satoshis: outputSatoshis1,
lockingScript: new P2PKH().lock(derivedAddress1).toHex(),
outputDescription: 'pay fred 1st BRC-29 payment'
},
{
satoshis: outputSatoshis2,
lockingScript: new P2PKH().lock(derivedAddress2).toHex(),
outputDescription: 'pay fred 2nd BRC-29 payment'
},
{
satoshis: outputSatoshis3,
lockingScript: _tu.getLockP2PKH(fredsAddress).toHex(),
outputDescription: 'pay fred 3rd payment'
},
{
satoshis: outputSatoshis4,
lockingScript: _tu.getLockP2PKH(fredsAddress).toHex(),
outputDescription: 'pay fred 4th payment'
}
],
options: {
returnTXIDOnly: false,
randomizeOutputs: false,
signAndProcess: true,
noSend: true
}
}
const cr = await wallet.createAction(createArgs)
expect(cr.tx).toBeTruthy()
const internalizeArgs: InternalizeActionArgs = {
tx: cr.tx!,
outputs: [
{
outputIndex: 0,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: derivationPrefix,
derivationSuffix: derivationSuffix1,
senderIdentityKey: senderIdentityKey
}
},
{
outputIndex: 1,
protocol: 'wallet payment',
paymentRemittance: {
derivationPrefix: derivationPrefix,
derivationSuffix: derivationSuffix2,
senderIdentityKey: senderIdentityKey
}
},
{
outputIndex: 2,
protocol: 'basket insertion',
insertionRemittance: {
basket: 'payments',
customInstructions: `3rd payment ${JSON.stringify({ root, repeat: 8 })}`,
tags: ['basket payments', '1st basket payment']
}
},
{
outputIndex: 3,
protocol: 'basket insertion',
insertionRemittance: {
basket: 'payments',
customInstructions: `4th payment ${JSON.stringify({ root, repeat: 8 })}`,
tags: ['basket payments', '2nd basket payment']
}
}
],
description: 'received 2 BRC-29 pay and 2 basket ins!'
}
const ir = await fred.wallet.internalizeAction(internalizeArgs)
expect(ir.accepted).toBe(true)
const rfbs = await fred.activeStorage.findOutputBaskets({
partial: { name: 'default' }
})
expect(rfbs.length).toBe(1)
const rfos = await fred.activeStorage.findOutputs({
partial: { basketId: rfbs[0].basketId }
})
expect(rfos.length).toBe(2)
expect(rfos[0].satoshis).toBe(outputSatoshis1)
expect(rfos[0].type).toBe('P2PKH')
expect(rfos[0].purpose).toBe('change')
expect(rfos[1].satoshis).toBe(outputSatoshis2)
expect(rfos[1].type).toBe('P2PKH')
expect(rfos[1].purpose).toBe('change')
{
const ro = await fred.activeStorage.findOutputs({
partial: { outputId: 3 }
})
expect(ro[0].basketId).toBe(2)
expect(ro[0].satoshis).toBe(outputSatoshis3)
expect(ro[0].customInstructions).toBe(`3rd payment ${JSON.stringify({ root, repeat: 8 })}`)
const rtm = await fred.activeStorage.findOutputTagMaps({
partial: { outputId: 3 }
})
const rt1 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[0].outputTagId }
})
expect(rt1[0].tag).toBe('basket payments')
const rt2 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[1].outputTagId }
})
expect(rt2[0].tag).toBe('1st basket payment')
}
{
const ro = await fred.activeStorage.findOutputs({
partial: { outputId: 4 }
})
expect(ro[0].basketId).toBe(2)
expect(ro[0].satoshis).toBe(outputSatoshis4)
expect(ro[0].customInstructions).toBe(`4th payment ${JSON.stringify({ root, repeat: 8 })}`)
const rtm = await fred.activeStorage.findOutputTagMaps({
partial: { outputId: 4 }
})
const rt1 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[0].outputTagId }
})
expect(rt1[0].tag).toBe('basket payments')
const rt2 = await fred.activeStorage.findOutputTags({
partial: { outputTagId: rtm[1].outputTagId }
})
expect(rt2[0].tag).toBe('2nd basket payment')
}
const r = await fred.wallet.internalizeAction(internalizeArgs)
await expect(Promise.resolve(r)).resolves.toBeTruthy()
await fred.activeStorage.destroy()
}
}
for (const ctx of ctxs) {
await ctx.storage.destroy()
}
})
})