UNPKG

@bsv/sdk

Version:

BSV Blockchain Software Development Kit

1,401 lines (1,335 loc) 86.7 kB
import { CompletedProtoWallet } from '../../../auth/certificates/__tests/CompletedProtoWallet' import { Utils, PrivateKey, Hash } from '../../../primitives/index' import WalletWireTransceiver from '../../../wallet/substrates/WalletWireTransceiver' import WalletWireProcessor from '../../../wallet/substrates/WalletWireProcessor' const sampleData = [3, 1, 4, 1, 5, 9] describe('WalletWire Integration Tests', () => { /** * This is a copy of the test suite for CompletedProtoWallet, but instead of using a CompletedProtoWallet directly, we're using it over the WalletWire. * This serves as an imperfect but still useful way to ensure that the WalletWire doesn't contain serialization or deserialization issues. */ describe('ProtoWallet Over Wallet Wire', () => { it('Throws when functions are not supported', async () => { const wallet = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet('anyone')) ) await expect(() => { return (wallet as any).createAction() }).rejects.toThrow() await expect(() => { return (wallet as any).abortAction() }).rejects.toThrow() await expect(() => { return (wallet as any).signAction() }).rejects.toThrow() await expect(() => { return (wallet as any).listOutputs() }).rejects.toThrow() await expect(() => { return (wallet as any).relinquishOutput() }).rejects.toThrow() await expect(() => { return (wallet as any).listActions() }).rejects.toThrow() await expect(() => { return (wallet as any).internalizeAction() }).rejects.toThrow() await expect(() => { return (wallet as any).acquireCertificate() }).rejects.toThrow() await expect(() => { return (wallet as any).proveCertificate() }).rejects.toThrow() await expect(() => { return (wallet as any).listCertificates() }).rejects.toThrow() await expect(() => { return (wallet as any).relinquishCertificate() }).rejects.toThrow() await expect(() => { return (wallet as any).getHeight() }).rejects.toThrow() await expect(() => { return (wallet as any).getHeaderForHeight() }).rejects.toThrow() // TODO: Remove these two from the throw list once they are implemented. await expect(() => { return (wallet as any).discoverByIdentityKey() }).rejects.toThrow() await expect(() => { return (wallet as any).discoverByAttributes() }).rejects.toThrow() }) it('Validates the BRC-3 compliance vector', async () => { const wallet = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet('anyone')) ) const { valid } = await wallet.verifySignature({ data: Utils.toArray('BRC-3 Compliance Validated!', 'utf8'), signature: [ 48, 68, 2, 32, 43, 34, 58, 156, 219, 32, 50, 70, 29, 240, 155, 137, 88, 60, 200, 95, 243, 198, 201, 21, 56, 82, 141, 112, 69, 196, 170, 73, 156, 6, 44, 48, 2, 32, 118, 125, 254, 201, 44, 87, 177, 170, 93, 11, 193, 134, 18, 70, 9, 31, 234, 27, 170, 177, 54, 96, 181, 140, 166, 196, 144, 14, 230, 118, 106, 105 ], protocolID: [2, 'BRC3 Test'], keyID: '42', counterparty: '0294c479f762f6baa97fbcd4393564c1d7bd8336ebd15928135bbcf575cd1a71a1' }) expect(valid).toBe(true) }) it('Validates the BRC-2 HMAC compliance vector', async () => { const wallet = new WalletWireTransceiver( new WalletWireProcessor( new CompletedProtoWallet( new PrivateKey( '6a2991c9de20e38b31d7ea147bf55f5039e4bbc073160f5e0d541d1f17e321b8', 'hex' ) ) ) ) const { valid } = await wallet.verifyHmac({ data: Utils.toArray('BRC-2 HMAC Compliance Validated!', 'utf8'), hmac: [ 81, 240, 18, 153, 163, 45, 174, 85, 9, 246, 142, 125, 209, 133, 82, 76, 254, 103, 46, 182, 86, 59, 219, 61, 126, 30, 176, 232, 233, 100, 234, 14 ], protocolID: [2, 'BRC2 Test'], keyID: '42', counterparty: '0294c479f762f6baa97fbcd4393564c1d7bd8336ebd15928135bbcf575cd1a71a1' }) expect(valid).toBe(true) }) it('Validates the BRC-2 Encryption compliance vector', async () => { const wallet = new WalletWireTransceiver( new WalletWireProcessor( new CompletedProtoWallet( new PrivateKey( '6a2991c9de20e38b31d7ea147bf55f5039e4bbc073160f5e0d541d1f17e321b8', 'hex' ) ) ) ) const { plaintext } = await wallet.decrypt({ ciphertext: [ 252, 203, 216, 184, 29, 161, 223, 212, 16, 193, 94, 99, 31, 140, 99, 43, 61, 236, 184, 67, 54, 105, 199, 47, 11, 19, 184, 127, 2, 165, 125, 9, 188, 195, 196, 39, 120, 130, 213, 95, 186, 89, 64, 28, 1, 80, 20, 213, 159, 133, 98, 253, 128, 105, 113, 247, 197, 152, 236, 64, 166, 207, 113, 134, 65, 38, 58, 24, 127, 145, 140, 206, 47, 70, 146, 84, 186, 72, 95, 35, 154, 112, 178, 55, 72, 124 ], protocolID: [2, 'BRC2 Test'], keyID: '42', counterparty: '0294c479f762f6baa97fbcd4393564c1d7bd8336ebd15928135bbcf575cd1a71a1' }) expect(Utils.toUTF8(plaintext)).toEqual( 'BRC-2 Encryption Compliance Validated!' ) }) it('Encrypts messages decryptable by the counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { ciphertext } = await user.encrypt({ plaintext: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) const { plaintext } = await counterparty.decrypt({ ciphertext, protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) expect(plaintext).toEqual(sampleData) expect(ciphertext).not.toEqual(plaintext) }) it('Fails to decryupt messages for the wrong protocol, key, and counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { ciphertext } = await user.encrypt({ plaintext: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) await expect( async () => await counterparty.decrypt({ ciphertext, protocolID: [1, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.decrypt({ ciphertext, protocolID: [2, 'tests'], keyID: '5', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.decrypt({ ciphertext, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) ).rejects.toThrow() }) it('Correctly derives keys for a counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { publicKey: identityKey } = await user.getPublicKey({ identityKey: true }) expect(identityKey).toEqual(userKey.toPublicKey().toString()) const { publicKey: derivedForCounterparty } = await user.getPublicKey({ protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) const { publicKey: derivedByCounterparty } = await counterparty.getPublicKey({ protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString(), forSelf: true }) expect(derivedForCounterparty).toEqual(derivedByCounterparty) }) it('Signs messages verifiable by the counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { signature } = await user.createSignature({ data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) const { valid } = await counterparty.verifySignature({ signature, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) expect(valid).toEqual(true) expect(signature.length).not.toEqual(0) }) it('Directly signs hash of message verifiable by the counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { signature } = await user.createSignature({ hashToDirectlySign: Hash.sha256(sampleData), protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) const { valid } = await counterparty.verifySignature({ signature, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) expect(valid).toEqual(true) const { valid: hashValid } = await counterparty.verifySignature({ signature, hashToDirectlyVerify: Hash.sha256(sampleData), protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) expect(hashValid).toEqual(true) expect(signature.length).not.toEqual(0) }) it('Fails to verify signature for the wrong data, protocol, key, and counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { signature } = await user.createSignature({ data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) await expect( async () => await counterparty.verifySignature({ signature, data: [0, ...sampleData], protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.verifySignature({ signature, data: sampleData, protocolID: [2, 'wrong'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.verifySignature({ signature, data: sampleData, protocolID: [2, 'tests'], keyID: '2', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.verifySignature({ signature, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) ).rejects.toThrow() }) it('Computes HMAC over messages verifiable by the counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { hmac } = await user.createHmac({ data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) const { valid } = await counterparty.verifyHmac({ hmac, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) expect(valid).toEqual(true) expect(hmac.length).toEqual(32) }) it('Fails to verify HMAC for the wrong data, protocol, key, and counterparty', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const counterparty = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(counterpartyKey)) ) const { hmac } = await user.createHmac({ data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) await expect( async () => await counterparty.verifyHmac({ hmac, data: [0, ...sampleData], protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.verifyHmac({ hmac, data: sampleData, protocolID: [2, 'wrong'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.verifyHmac({ hmac, data: sampleData, protocolID: [2, 'tests'], keyID: '2', counterparty: userKey.toPublicKey().toString() }) ).rejects.toThrow() await expect( async () => await counterparty.verifyHmac({ hmac, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: counterpartyKey.toPublicKey().toString() }) ).rejects.toThrow() }) it('Uses anyone for creating signatures and self for other operations if no counterparty is provided', async () => { const userKey = PrivateKey.fromRandom() const user = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(userKey)) ) const { hmac } = await user.createHmac({ data: sampleData, protocolID: [2, 'tests'], keyID: '4' }) const { valid: hmacValid } = await user.verifyHmac({ hmac, data: sampleData, protocolID: [2, 'tests'], keyID: '4' }) expect(hmacValid).toEqual(true) const { valid: explicitSelfHmacValid } = await user.verifyHmac({ hmac, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: 'self' }) expect(explicitSelfHmacValid).toEqual(true) expect(hmac.length).toEqual(32) const { signature: anyoneSig } = await user.createSignature({ data: sampleData, protocolID: [2, 'tests'], keyID: '4' // counterparty=anyone is implicit for creating signatures }) const anyone = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet('anyone')) ) const { valid: anyoneSigValid } = await anyone.verifySignature({ signature: anyoneSig, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: userKey.toPublicKey().toString() }) expect(anyoneSigValid).toEqual(true) const { signature: selfSig } = await user.createSignature({ data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: 'self' }) const { valid: selfSigValid } = await user.verifySignature({ signature: selfSig, data: sampleData, protocolID: [2, 'tests'], keyID: '4' // Self is implicit when verifying signatures }) expect(selfSigValid).toEqual(true) const { valid: explicitSelfSigValid } = await user.verifySignature({ signature: selfSig, data: sampleData, protocolID: [2, 'tests'], keyID: '4', counterparty: 'self' }) expect(explicitSelfSigValid).toEqual(true) const { publicKey } = await user.getPublicKey({ protocolID: [2, 'tests'], keyID: '4' }) const { publicKey: explicitSelfPublicKey } = await user.getPublicKey({ protocolID: [2, 'tests'], keyID: '4', counterparty: 'self' }) expect(publicKey).toEqual(explicitSelfPublicKey) const { ciphertext } = await user.encrypt({ plaintext: sampleData, protocolID: [2, 'tests'], keyID: '4' }) const { plaintext } = await user.decrypt({ ciphertext, protocolID: [2, 'tests'], keyID: '4' }) const { plaintext: explicitSelfPlaintext } = await user.decrypt({ ciphertext, protocolID: [2, 'tests'], keyID: '4', counterparty: 'self' }) expect(plaintext).toEqual(explicitSelfPlaintext) expect(plaintext).toEqual(sampleData) }) it('Validates the revealCounterpartyKeyLinkage function', async () => { // Initialize keys const proverKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const verifierKey = PrivateKey.fromRandom() // Initialize wallets const proverWallet = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(proverKey)) ) const verifierWallet = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(verifierKey)) ) // Prover reveals counterparty key linkage const revelation = await proverWallet.revealCounterpartyKeyLinkage({ counterparty: counterpartyKey.toPublicKey().toString(), verifier: verifierKey.toPublicKey().toString() }) // Verifier decrypts the encrypted linkage const { plaintext: linkage } = await verifierWallet.decrypt({ ciphertext: revelation.encryptedLinkage, protocolID: [2, 'counterparty linkage revelation'], keyID: revelation.revelationTime, counterparty: proverKey.toPublicKey().toString() }) // Compute expected linkage const expectedLinkage = proverKey .deriveSharedSecret(counterpartyKey.toPublicKey()) .encode(true) // Compare linkage and expectedLinkage expect(linkage).toEqual(expectedLinkage) }) it('Validates the revealSpecificKeyLinkage function', async () => { // Initialize keys const proverKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const verifierKey = PrivateKey.fromRandom() // Initialize wallets const proverWallet = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(proverKey)) ) const verifierWallet = new WalletWireTransceiver( new WalletWireProcessor(new CompletedProtoWallet(verifierKey)) ) const protocolID: [0 | 1 | 2, string] = [0, 'tests'] const keyID = 'test key id' // Prover reveals specific key linkage const revelation = await proverWallet.revealSpecificKeyLinkage({ counterparty: counterpartyKey.toPublicKey().toString(), verifier: verifierKey.toPublicKey().toString(), protocolID, keyID }) expect(revelation.encryptedLinkageProof).toBeDefined() expect(revelation.proofType).toBeDefined() // Verifier decrypts the encrypted linkage const { plaintext: linkage } = await verifierWallet.decrypt({ ciphertext: revelation.encryptedLinkage, protocolID: [ 2, `specific linkage revelation ${protocolID[0]} ${protocolID[1]}` ], keyID, counterparty: proverKey.toPublicKey().toString() }) // Compute expected linkage const sharedSecret = proverKey .deriveSharedSecret(counterpartyKey.toPublicKey()) .encode(true) // Function to compute the invoice number const computeInvoiceNumber = function (protocolID: [number, string], keyID: string): string { const securityLevel = protocolID[0] if ( !Number.isInteger(securityLevel) || securityLevel < 0 || securityLevel > 2 ) { throw new Error('Protocol security level must be 0, 1, or 2') } const protocolName = protocolID[1].toLowerCase().trim() if (keyID.length > 800) { throw new Error('Key IDs must be 800 characters or less') } if (keyID.length < 1) { throw new Error('Key IDs must be 1 character or more') } if (protocolName.length > 400) { throw new Error('Protocol names must be 400 characters or less') } if (protocolName.length < 5) { throw new Error('Protocol names must be 5 characters or more') } if (protocolName.includes(' ')) { throw new Error( 'Protocol names cannot contain multiple consecutive spaces (" ")' ) } if (!/^[a-z0-9 ]+$/g.test(protocolName)) { throw new Error( 'Protocol names can only contain letters, numbers and spaces' ) } if (protocolName.endsWith(' protocol')) { throw new Error('No need to end your protocol name with " protocol"') } return `${securityLevel}-${protocolName}-${keyID}` } const invoiceNumber = computeInvoiceNumber(protocolID, keyID) const invoiceNumberBin = Utils.toArray(invoiceNumber, 'utf8') // Compute expected linkage const expectedLinkage = Hash.sha256hmac(sharedSecret, invoiceNumberBin) // Compare linkage and expectedLinkage expect(linkage).toEqual(expectedLinkage) }) }) // Helper function to create a test wallet wire setup const createTestWalletWire = (wallet: CompletedProtoWallet): WalletWireTransceiver => { const processor = new WalletWireProcessor(wallet) const transceiver = new WalletWireTransceiver(processor) return transceiver } // Mock implementation for methods not supported by CompletedProtoWallet const mockUnsupportedMethods = ( methods: Partial<CompletedProtoWallet> ): CompletedProtoWallet => { // @ts-expect-error const result: CompletedProtoWallet = { ...methods } return result } describe('createAction', () => { it('should create an action with valid inputs', async () => { // Mock the createAction method const createActionMock = jest.fn().mockResolvedValue({ txid: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806', tx: [1, 2, 3, 4] }) const wallet = createTestWalletWire( mockUnsupportedMethods({ createAction: createActionMock }) ) const args = { description: 'Test action description', outputs: [ { lockingScript: '00', // Sample locking script satoshis: 1000, outputDescription: 'Test output', basket: 'test-basket', customInstructions: 'Test instructions', tags: ['test-tag'] } ], labels: ['test-label'] } const result = await wallet.createAction(args) expect(result).toHaveProperty('txid') expect(result).toHaveProperty('tx') expect(result.tx).toBeInstanceOf(Array) expect(createActionMock).toHaveBeenCalledWith(args, '') }) it('should create an action with minimal inputs (only description)', async () => { // Mock the createAction method const createActionMock = jest.fn().mockResolvedValue({ txid: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806' }) const wallet = createTestWalletWire( mockUnsupportedMethods({ createAction: createActionMock }) ) const args = { description: 'Minimal action description' } const result = await wallet.createAction(args) expect(result).toHaveProperty('txid') expect(result).not.toHaveProperty('tx') expect(result).not.toHaveProperty('noSendChange') expect(result).not.toHaveProperty('sendWithResults') expect(result).not.toHaveProperty('signableTransaction') expect(createActionMock).toHaveBeenCalledWith(args, '') }) it('should create an action and return only txid when returnTXIDOnly is true', async () => { // Mock the createAction method const createActionMock = jest.fn().mockResolvedValue({ txid: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806' }) const wallet = createTestWalletWire( mockUnsupportedMethods({ createAction: createActionMock }) ) const args = { description: 'Test action with returnTXIDOnly', options: { returnTXIDOnly: true } } const result = await wallet.createAction(args) expect(result).toHaveProperty('txid') expect(result).not.toHaveProperty('tx') expect(result).not.toHaveProperty('noSendChange') expect(result).not.toHaveProperty('sendWithResults') expect(result).not.toHaveProperty('signableTransaction') expect(createActionMock).toHaveBeenCalledWith(args, '') }) it('should create an action and return a signableTransaction when noSend is true', async () => { // Mock the createAction method const createActionMock = jest.fn().mockResolvedValue({ signableTransaction: { tx: [0x01], reference: Utils.toBase64([0x01]) } }) const wallet = createTestWalletWire( mockUnsupportedMethods({ createAction: createActionMock }) ) const args = { description: 'Test action with noSend', options: { noSend: true } } const result = await wallet.createAction(args) expect(result).toHaveProperty('signableTransaction') expect(result.signableTransaction).toHaveProperty('tx') expect(result.signableTransaction).toHaveProperty('reference') expect(result).not.toHaveProperty('txid') expect(result).not.toHaveProperty('tx') expect(createActionMock).toHaveBeenCalledWith(args, '') }) it('should create an action with all options set and handle all return values', async () => { // Mock the createAction method const createActionMock = jest.fn().mockResolvedValue({ txid: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806', tx: [1, 2, 3, 4], noSendChange: [ 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806.0' ], sendWithResults: [ { txid: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806', status: 'sending' } ], signableTransaction: { tx: [0x01], reference: Utils.toBase64([0x01]) } }) const wallet = createTestWalletWire( mockUnsupportedMethods({ createAction: createActionMock }) ) const args = { description: 'Test action with all options', inputs: [], inputBEEF: [1, 2, 3, 4], outputs: [ { lockingScript: '016a', satoshis: 1, outputDescription: 'This is a test.' } ], lockTime: 0, version: 1, labels: ['label1', 'label2'], options: { signAndProcess: false, acceptDelayedBroadcast: false, trustSelf: 'known' as 'known', knownTxids: [ 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806' ], returnTXIDOnly: false, noSend: true, noSendChange: [ 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806.0' ], sendWith: [ 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806' ], randomizeOutputs: false } } const result = await wallet.createAction(args) expect(result).toHaveProperty('txid') expect(result).toHaveProperty('tx') expect(result).toHaveProperty('noSendChange') expect(result).toHaveProperty('sendWithResults') expect(result).toHaveProperty('signableTransaction') expect(createActionMock).toHaveBeenCalledWith(args, '') }) it('should throw an error with invalid inputs', async () => { // Mock the createAction method to throw an error const createActionMock = jest .fn() .mockRejectedValue(new Error('Invalid inputs')) const wallet = createTestWalletWire( mockUnsupportedMethods({ createAction: createActionMock }) ) const args = { description: '' // Invalid description (too short) } await expect(wallet.createAction(args)).rejects.toThrow('Invalid inputs') expect(createActionMock).toHaveBeenCalledWith(args, '') }) }) describe('signAction', () => { it('should sign an action with valid inputs', async () => { // Mock the signAction method const signActionMock = jest.fn().mockResolvedValue({ txid: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806', tx: [1, 2, 3, 4] }) const wallet = createTestWalletWire( mockUnsupportedMethods({ signAction: signActionMock }) ) const spends = { 0: { unlockingScript: '00' // Sample unlocking script } } const reference = Utils.toBase64([1, 2, 3]) const args = { spends, reference } const result = await wallet.signAction(args) expect(result).toHaveProperty('txid') expect(result).toHaveProperty('tx') expect(result.tx).toBeInstanceOf(Array) expect(signActionMock).toHaveBeenCalledWith(args, '') }) it('should throw an error with invalid inputs', async () => { // Mock the signAction method to throw an error const signActionMock = jest .fn() .mockRejectedValue(new Error('Invalid inputs')) const wallet = createTestWalletWire( mockUnsupportedMethods({ signAction: signActionMock }) ) const spends = {} const reference = '' const args = { spends, reference } await expect(wallet.signAction(args)).rejects.toThrow('Invalid inputs') expect(signActionMock).toHaveBeenCalledWith(args, '') }) }) describe('abortAction', () => { it('should abort an action with valid reference', async () => { // Mock the abortAction method const abortActionMock = jest.fn().mockResolvedValue({ aborted: true }) const wallet = createTestWalletWire( mockUnsupportedMethods({ abortAction: abortActionMock }) ) const reference = Utils.toBase64([1, 2, 3]) const args = { reference } const result = await wallet.abortAction(args) expect(result).toEqual({ aborted: true }) expect(abortActionMock).toHaveBeenCalledWith(args, '') }) it('should throw an error with invalid reference', async () => { // Mock the abortAction method to throw an error const abortActionMock = jest .fn() .mockRejectedValue(new Error('Invalid reference')) const wallet = createTestWalletWire( mockUnsupportedMethods({ abortAction: abortActionMock }) ) const reference = '' const args = { reference } await expect(wallet.abortAction(args)).rejects.toThrow( 'Invalid reference' ) expect(abortActionMock).toHaveBeenCalledWith(args, '') }) }) describe('listActions', () => { it('should list actions with valid inputs', async () => { // Mock the listActions method const listActionsMock = jest.fn().mockResolvedValue({ totalActions: 1, actions: [ { txid: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806', satoshis: 1000, status: 'completed', isOutgoing: true, description: 'Test action', labels: ['test-label'], version: 1, lockTime: 0 } ] }) const wallet = createTestWalletWire( mockUnsupportedMethods({ listActions: listActionsMock }) ) const args = { labels: ['test-label'], includeLabels: true, limit: 10, offset: 0 } const result = await wallet.listActions(args) expect(result).toHaveProperty('totalActions') expect(result).toHaveProperty('actions') expect(Array.isArray(result.actions)).toBe(true) expect(listActionsMock).toHaveBeenCalledWith(args, '') }) it('should list actions with empty labels array', async () => { // Mock the listActions method const listActionsMock = jest.fn().mockResolvedValue({ totalActions: 0, actions: [] }) const wallet = createTestWalletWire( mockUnsupportedMethods({ listActions: listActionsMock }) ) const args = { labels: [], includeLabels: true, limit: 10, offset: 0 } const result = await wallet.listActions(args) expect(result).toHaveProperty('totalActions') expect(result.totalActions).toBe(0) expect(result.actions).toEqual([]) expect(listActionsMock).toHaveBeenCalledWith(args, '') }) it('should throw an error with invalid inputs', async () => { // Mock the listActions method to throw an error const listActionsMock = jest .fn() .mockRejectedValue(new Error('Invalid inputs')) const wallet = createTestWalletWire( mockUnsupportedMethods({ listActions: listActionsMock }) ) const args = { labels: [] } await expect(wallet.listActions(args)).rejects.toThrow('Invalid inputs') expect(listActionsMock).toHaveBeenCalledWith(args, '') }) }) describe('internalizeAction', () => { it('should internalize an action with valid inputs', async () => { // Mock the internalizeAction method const internalizeActionMock = jest .fn() .mockResolvedValue({ accepted: true }) const wallet = createTestWalletWire( mockUnsupportedMethods({ internalizeAction: internalizeActionMock }) ) const args = { tx: [0x00], // Sample transaction byte array outputs: [ { outputIndex: 0, protocol: 'wallet payment' as 'wallet payment', paymentRemittance: { derivationPrefix: Utils.toBase64([1, 2, 3]), derivationSuffix: Utils.toBase64([4, 5, 6]), senderIdentityKey: '02' + '1'.repeat(64) } } ], description: 'Test internalize action', labels: ['test-label'] } const result = await wallet.internalizeAction(args) expect(result).toEqual({ accepted: true }) expect(internalizeActionMock).toHaveBeenCalledWith(args, '') }) it('should throw an error with invalid inputs', async () => { // Mock the internalizeAction method to throw an error const internalizeActionMock = jest .fn() .mockRejectedValue(new Error('Invalid inputs')) const wallet = createTestWalletWire( mockUnsupportedMethods({ internalizeAction: internalizeActionMock }) ) const args = { tx: [], // Empty transaction array outputs: [], description: 'Test internalize action' } await expect(wallet.internalizeAction(args)).rejects.toThrow( 'Invalid inputs' ) expect(internalizeActionMock).toHaveBeenCalledWith(args, '') }) it('should internalize an action with "basket insertion" protocol', async () => { // Mock the internalizeAction method const internalizeActionMock = jest .fn() .mockResolvedValue({ accepted: true }) const wallet = createTestWalletWire( mockUnsupportedMethods({ internalizeAction: internalizeActionMock }) ) const args = { tx: [0x00], // Sample transaction byte array outputs: [ { outputIndex: 0, protocol: 'basket insertion' as 'basket insertion', insertionRemittance: { basket: 'test-basket', customInstructions: 'Test instructions', tags: ['test-tag1', 'test-tag2'] } } ], description: 'Test internalize action with basket insertion', labels: ['test-label'] } const result = await wallet.internalizeAction(args) expect(result).toEqual({ accepted: true }) expect(internalizeActionMock).toHaveBeenCalledWith(args, '') }) }) describe('listOutputs', () => { it('should list outputs with valid inputs', async () => { // Mock the listOutputs method const listOutputsMock = jest.fn().mockResolvedValue({ totalOutputs: 1, outputs: [ { outpoint: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806.0', satoshis: 1000, lockingScript: '00', spendable: true, customInstructions: 'Test instructions', tags: ['test-tag'], labels: ['test-label'] } ] }) const wallet = createTestWalletWire( mockUnsupportedMethods({ listOutputs: listOutputsMock }) ) const args = { basket: 'test-basket', includeLabels: true, limit: 10, offset: 0 } const result = await wallet.listOutputs(args) expect(result).toHaveProperty('totalOutputs') expect(result).toHaveProperty('outputs') expect(Array.isArray(result.outputs)).toBe(true) expect(listOutputsMock).toHaveBeenCalledWith(args, '') }) it('should throw an error with invalid inputs', async () => { // Mock the listOutputs method to throw an error const listOutputsMock = jest .fn() .mockRejectedValue(new Error('Invalid inputs')) const wallet = createTestWalletWire( mockUnsupportedMethods({ listOutputs: listOutputsMock }) ) const args = { basket: '' } await expect(wallet.listOutputs(args)).rejects.toThrow('Invalid inputs') expect(listOutputsMock).toHaveBeenCalledWith(args, '') }) it('should list outputs without specifying optional parameters', async () => { // Mock the listOutputs method const listOutputsMock = jest.fn().mockResolvedValue({ totalOutputs: 1, BEEF: [1, 2, 3, 4], outputs: [ { outpoint: 'deadbeef20248806deadbeef20248806deadbeef20248806deadbeef20248806.0', satoshis: 1000, spendable: true } ] }) const wallet = createTestWalletWire( mockUnsupportedMethods({ listOutputs: listOutputsMock }) ) const args = { basket: 'test-basket' // Optional parameters are not specified } const result = await wallet.listOutputs(args) expect(result).toHaveProperty('totalOutputs') expect(result).toHaveProperty('outputs') expect(result).toHaveProperty('BEEF') expect(result.outputs[0]).toHaveProperty('outpoint') expect(result.outputs[0]).toHaveProperty('satoshis') expect(result.outputs[0]).toHaveProperty('spendable') expect(result.outputs[0]).not.toHaveProperty('lockingScript') expect(result.outputs[0]).not.toHaveProperty('customInstructions') expect(result.outputs[0]).not.toHaveProperty('tags') expect(result.outputs[0]).not.toHaveProperty('labels') expect(listOutputsMock).toHaveBeenCalledWith(args, '') }) }) describe('getPublicKey', () => { it('should get the identity public key', async () => { const wallet = createTestWalletWire( new CompletedProtoWallet(PrivateKey.fromRandom()) ) const result = await wallet.getPublicKey({ identityKey: true }) expect(result).toHaveProperty('publicKey') expect(typeof result.publicKey).toBe('string') expect(result.publicKey.length).toBe(66) // Compressed public key hex length }) it('should get a derived public key with valid inputs', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const wallet = createTestWalletWire(new CompletedProtoWallet(userKey)) const args = { protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: counterpartyKey.toPublicKey().toString() } const result = await wallet.getPublicKey(args) expect(result).toHaveProperty('publicKey') expect(typeof result.publicKey).toBe('string') expect(result.publicKey.length).toBe(66) }) it('should get the public key with counterparty "anyone"', async () => { const wallet = createTestWalletWire( new CompletedProtoWallet(PrivateKey.fromRandom()) ) const args = { protocolID: [1, 'testprotocol'] as [0 | 1 | 2, string], keyID: 'testkeyid', counterparty: 'anyone' as 'anyone' } const result = await wallet.getPublicKey(args) expect(result).toHaveProperty('publicKey') expect(typeof result.publicKey).toBe('string') expect(result.publicKey.length).toBe(66) // Compressed public key hex length }) it('should get the public key with missing optional parameters', async () => { const wallet = createTestWalletWire( new CompletedProtoWallet(PrivateKey.fromRandom()) ) const args = { protocolID: [0, 'minimalprotocol'] as [0 | 1 | 2, string], keyID: 'minimalkeyid' // Missing counterparty, should default to 'self' or 'anyone' based on context } const result = await wallet.getPublicKey(args) expect(result).toHaveProperty('publicKey') expect(typeof result.publicKey).toBe('string') expect(result.publicKey.length).toBe(66) }) }) describe('encrypt and decrypt', () => { it('should encrypt and decrypt data correctly', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const userWallet = createTestWalletWire(new CompletedProtoWallet(userKey)) const counterpartyWallet = createTestWalletWire( new CompletedProtoWallet(counterpartyKey) ) const plaintext = sampleData const encryptArgs = { plaintext, protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: counterpartyKey.toPublicKey().toString() } const encryptResult = await userWallet.encrypt(encryptArgs) expect(encryptResult).toHaveProperty('ciphertext') expect(encryptResult.ciphertext).not.toEqual(plaintext) const decryptArgs = { ciphertext: encryptResult.ciphertext, protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: userKey.toPublicKey().toString() } const decryptResult = await counterpartyWallet.decrypt(decryptArgs) expect(decryptResult).toHaveProperty('plaintext') expect(decryptResult.plaintext).toEqual(plaintext) }) it('should throw an error for invalid decryption inputs', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const counterpartyWallet = createTestWalletWire( new CompletedProtoWallet(counterpartyKey) ) const decryptArgs = { ciphertext: [0x00], protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: userKey.toPublicKey().toString() } await expect(counterpartyWallet.decrypt(decryptArgs)).rejects.toThrow() }) }) describe('createHmac and verifyHmac', () => { it('should create and verify HMAC correctly', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const userWallet = createTestWalletWire(new CompletedProtoWallet(userKey)) const counterpartyWallet = createTestWalletWire( new CompletedProtoWallet(counterpartyKey) ) const data = sampleData const createHmacArgs = { data, protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: counterpartyKey.toPublicKey().toString() } const createHmacResult = await userWallet.createHmac(createHmacArgs) expect(createHmacResult).toHaveProperty('hmac') expect(createHmacResult.hmac.length).toBe(32) const verifyHmacArgs = { data, hmac: createHmacResult.hmac, protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: userKey.toPublicKey().toString() } const verifyHmacResult = await counterpartyWallet.verifyHmac(verifyHmacArgs) expect(verifyHmacResult).toEqual({ valid: true }) }) it('should throw an error for invalid HMAC verification', async () => { const userKey = PrivateKey.fromRandom() const counterpartyWallet = createTestWalletWire( new CompletedProtoWallet(PrivateKey.fromRandom()) ) const verifyHmacArgs = { data: sampleData, hmac: [0x00], protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: userKey.toPublicKey().toString() } await expect( counterpartyWallet.verifyHmac(verifyHmacArgs) ).rejects.toThrow() }) }) describe('createSignature and verifySignature', () => { it('should create and verify signature correctly', async () => { const userKey = PrivateKey.fromRandom() const counterpartyKey = PrivateKey.fromRandom() const userWallet = createTestWalletWire(new CompletedProtoWallet(userKey)) const counterpartyWallet = createTestWalletWire( new CompletedProtoWallet(counterpartyKey) ) const data = sampleData const createSignatureArgs = { data, protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: counterpartyKey.toPublicKey().toString() } const createSignatureResult = await userWallet.createSignature(createSignatureArgs) expect(createSignatureResult).toHaveProperty('signature') expect(createSignatureResult.signature.length).toBeGreaterThan(0) const verifySignatureArgs = { data, signature: createSignatureResult.signature, protocolID: [2, 'tests'] as [0 | 1 | 2, string], keyID: 'test-key-id', counterparty: userKey.toPublicKey().toString() } const verifySignatureResult = await counterpartyWallet.verifySignature(verifySignatureArgs) expect(verifySignatureResult).toEqual({ valid: true }) }) it('should throw an error for invalid signature verification', async () => { const userKey = PrivateKey.fromRandom() const counterpartyWallet = createTestWalletWire( new CompletedProtoWallet(PrivateKey.fromRandom()) ) const verifySignatureArgs = { data: sampleData, signature: [0x00], protoc