diff --git a/__tests__/bid.test.ts b/__tests__/bid.test.ts index c87954b71..738426cea 100644 --- a/__tests__/bid.test.ts +++ b/__tests__/bid.test.ts @@ -2,7 +2,7 @@ import * from 'jest'; import { OpenMarketProtocol } from '../src/omp'; import { Cryptocurrency } from '../src/interfaces/crypto'; import { BidConfiguration } from '../src/interfaces/configs'; -import { EscrowType } from '../src/interfaces/omp-enums'; +import { EscrowType, MPAction } from '../src/interfaces/omp-enums'; import { CoreRpcService } from '../test/rpc.stub'; describe('Bid', () => { @@ -11,7 +11,7 @@ describe('Bid', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_LISTING_ADD", + "type": "${MPAction.MPA_LISTING_ADD}", "item": { "information": { "title": "a 6 month old dog", diff --git a/__tests__/buyflow/madct.test.ts b/__tests__/buyflow/madct.test.ts index c04ee78ec..3e2f043de 100644 --- a/__tests__/buyflow/madct.test.ts +++ b/__tests__/buyflow/madct.test.ts @@ -1,14 +1,19 @@ import * from 'jest'; import { CtCoreRpcService } from '../../test/rpc-ct.stub'; import { OpenMarketProtocol } from '../../src/omp'; -import { Cryptocurrency } from '../../src/interfaces/crypto'; +import { Cryptocurrency, OutputType } from '../../src/interfaces/crypto'; import { BidConfiguration } from '../../src/interfaces/configs'; -import { EscrowType } from '../../src/interfaces/omp-enums'; +import { EscrowType, MPAction } from '../../src/interfaces/omp-enums'; import { toSatoshis, strip, log } from '../../src/util'; import { Rpc } from '../../src/abstract/rpc'; +import { RpcUnspentOutput } from '../../src/interfaces/rpc'; +import delay from 'delay'; describe('Buyflow: mad ct', () => { + const WALLET = ''; // use the default wallet + const SECOND_WALLET = 'test-wallet'; // second wallet for the purpose of testing multiwallet + let buyer: OpenMarketProtocol; let seller: OpenMarketProtocol; @@ -27,13 +32,30 @@ describe('Buyflow: mad ct', () => { seller = new OpenMarketProtocol({ network: 'testnet'}); seller.inject(Cryptocurrency.PART, sellerNode1); + + if (!(await buyerNode0.walletExists(SECOND_WALLET))) { + await buyerNode0.createWallet(SECOND_WALLET, false, true); + await buyerNode0.setupWallet(SECOND_WALLET); + } + if (!(await sellerNode1.walletExists(SECOND_WALLET))) { + await sellerNode1.createWallet(SECOND_WALLET, false, true); + await sellerNode1.setupWallet(SECOND_WALLET); + } + + if (!(await buyerNode0.walletLoaded(SECOND_WALLET))) { + await buyerNode0.loadWallet(SECOND_WALLET); + } + if (!(await sellerNode1.walletLoaded(SECOND_WALLET))) { + await sellerNode1.loadWallet(SECOND_WALLET); + } + }); const ok = JSON.parse( `{ - "version": "0.1.0.0", + "version": "0.3.0", "action": { - "type": "MPA_LISTING_ADD", + "type": "${MPAction.MPA_LISTING_ADD}", "item": { "information": { "title": "a 6 month old dog", @@ -43,6 +65,10 @@ describe('Buyflow: mad ct', () => { "Animals" ] }, + "seller": { + "address": "pVHUY9AYwNSjbX8f1d4fPgYCkNmZxMC25p", + "signature": "H7yN04IMrwbUgqFXT5Jzr5BPS5vpNrc9deKaY6jkCh0icM5Z3V5rtle/EkugQccw0vk/K6CReQ8sSSDo5W9Vl1I=" + }, "payment": { "type": "SALE", "escrow": { @@ -94,29 +120,31 @@ describe('Buyflow: mad ct', () => { // Step 1: Buyer does bid log(' >>>>> Step 1: Buyer does bid'); - const bid = await buyer.bid(config, ok); + const bid = await buyer.bid(WALLET, config, ok); const bid_stripped = strip(bid); - await delay(7000); + + const time = Date.now(); + await delay(10000); // Step 2: seller accepts log(' >>>>> Step 2: seller accepts'); - const accept = await seller.accept(ok, bid_stripped); + const accept = await seller.accept(WALLET, ok, bid_stripped); const accept_stripped = strip(accept); - expect(accept.action['_rawdesttx']).not.toBeCompletedTransaction(); + await expectCompletedTransaction(accept.action['_rawdesttx'], false); // Step 3: buyer signs destroy txn (done), signs bid txn (half) log(' >>>>> Step 3: buyer signs destroy txn (done), signs bid txn (half)'); - await delay(7000); - const lock = await buyer.lock(ok, bid, accept_stripped); + await delay(10000); + const lock = await buyer.lock(WALLET, ok, bid, accept_stripped); const lock_stripped = strip(lock); - expect(lock.action['_rawdesttx']).not.toBeCompletedTransaction(); + await expectCompletedTransaction(lock.action['_rawdesttx'], false); expect(lock.action['_rawreleasetxunsigned']).toEqual(accept.action['_rawreleasetxunsigned']); // Step 4: seller signs bid txn (full) and submits log(' >>>>> Step 4: seller signs bid txn (full) and submits'); - const complete = await seller.complete(ok, bid_stripped, accept_stripped, lock_stripped); - expect(complete).toBeCompletedTransaction(); + const complete = await seller.complete(WALLET, ok, bid_stripped, accept_stripped, lock_stripped); + await expectCompletedTransaction(complete); const completeTxid = await buyerNode0.sendRawTransaction(complete); await sellerNode1.sendRawTransaction(complete); @@ -124,17 +152,24 @@ describe('Buyflow: mad ct', () => { // Step 5: buyer signs release log(' >>>>> Step 5: buyer signs release'); - await delay(10000); - const release = await buyer.release(ok, bid, accept); - expect(release).toBeCompletedTransaction(); + await delay(15000); + const release = await buyer.release(WALLET, ok, bid, accept); + await expectCompletedTransaction(release); const releaseTxid = await buyerNode0.sendRawTransaction(release); await sellerNode1.sendRawTransaction(release); expect(releaseTxid).toBeDefined(); + log('releaseTxid: ' + releaseTxid); - await delay(10000); - expect(releaseTxid).toBeUtxoWithAmount(buyerNode0, 2); - expect(releaseTxid).toBeUtxoWithAmount(sellerNode1, 3.99995000); + await delay(20000); // 10000 doesnt seem to be enough + await expectUtxoWithAmount(releaseTxid, buyerNode0, 2); + await expectUtxoWithAmount(releaseTxid, sellerNode1, 3.99995000); + + log('!!! buyflow release success!'); + + // await expect(releaseTxid).toBeUtxoWithAmount(buyerNode0, 2); + // await expect(releaseTxid).toBeUtxoWithAmount(sellerNode1, 3.99995000); + // await delay(2000); }, 600000); @@ -143,46 +178,52 @@ describe('Buyflow: mad ct', () => { // Step1: Buyer does bid log(' >>>>> Step 1: Buyer does bid'); - const bid = await buyer.bid(config, ok); + const bid = await buyer.bid(WALLET, config, ok); const bid_stripped = strip(bid); await delay(7000); // Step 2: seller accepts log(' >>>>> Step 2: seller accepts'); - const accept = await seller.accept(ok, bid_stripped); + const accept = await seller.accept(WALLET, ok, bid_stripped); const accept_stripped = strip(accept); - expect(accept.action['_rawdesttx']).not.toBeCompletedTransaction(); + await expectCompletedTransaction(accept.action['_rawdesttx'], false); await delay(7000); // Step 3: buyer signs destroy txn (done), signs bid txn (half) log(' >>>>> Step 3: buyer signs destroy txn (done), signs bid txn (half)'); - const lock = await buyer.lock(ok, bid, accept_stripped); + const lock = await buyer.lock(WALLET, ok, bid, accept_stripped); const lock_stripped = strip(lock); - expect(lock.action['_rawdesttx']).not.toBeCompletedTransaction(); + await expectCompletedTransaction(lock.action['_rawdesttx'], false); expect(lock.action['_rawreleasetxunsigned']).toEqual(accept.action['_rawreleasetxunsigned']); // Step 4: seller signs bid txn (full) and submits log(' >>>>> Step 4: seller signs bid txn (full) and submits'); - const complete = await seller.complete(ok, bid_stripped, accept_stripped, lock_stripped); - expect(complete).toBeCompletedTransaction(); + const complete = await seller.complete(WALLET, ok, bid_stripped, accept_stripped, lock_stripped); + await expectCompletedTransaction(complete, true); const completeTxid = await buyerNode0.sendRawTransaction(complete); await sellerNode1.sendRawTransaction(complete); expect(completeTxid).toBeDefined(); - await delay(10000); + await delay(15000); // Step 5: seller signs refund log(' >>>>> Step 5: seller signs refund'); - const refund = await seller.refund(ok, bid, accept, lock); - expect(refund).toBeCompletedTransaction(); + const refund = await seller.refund(WALLET, ok, bid, accept, lock); + await expectCompletedTransaction(refund, true); const refundTxid = await buyerNode0.sendRawTransaction(refund); await sellerNode1.sendRawTransaction(refund); expect(refundTxid).toBeDefined(); - await delay(10000); - expect(refundTxid).toBeUtxoWithAmount(buyerNode0, 4); - expect(refundTxid).toBeUtxoWithAmount(sellerNode1, 1.99995000); + log('refundTxid: ' + refundTxid); + await delay(20000); + // await expect(refundTxid).toBeUtxoWithAmount(buyerNode0, 4); + // await expect(refundTxid).toBeUtxoWithAmount(sellerNode1, 1.99995000); + await expectUtxoWithAmount(refundTxid, buyerNode0, 4); + await expectUtxoWithAmount(refundTxid, sellerNode1, 1.99995000); + // await delay(2000); + + log('!!! buyflow refund success!'); }, 600000); @@ -191,30 +232,30 @@ describe('Buyflow: mad ct', () => { // Step 1: Buyer does bid log(' >>>>> Step 1: Buyer does bid'); - const bid = await buyer.bid(config, ok); + const bid = await buyer.bid(WALLET, config, ok); const bid_stripped = strip(bid); await delay(10000); // Step 2: seller accepts log(' >>>>> Step 2: seller accepts'); - const accept = await seller.accept(ok, bid_stripped); + const accept = await seller.accept(WALLET, ok, bid_stripped); const accept_stripped = strip(accept); - expect(accept.action['_rawdesttx']).not.toBeCompletedTransaction(); + await expectCompletedTransaction(accept.action['_rawdesttx'], false); await delay(10000); // Step 3: buyer signs destroy txn (done), signs bid txn (half) log(' >>>>> Step 3: buyer signs destroy txn (done), signs bid txn (half)'); - const lock = await buyer.lock(ok, bid_stripped, accept_stripped); + const lock = await buyer.lock(WALLET, ok, bid_stripped, accept_stripped); const lock_stripped = strip(lock); - expect(lock.action['_rawdesttx']).not.toBeCompletedTransaction(); + await expectCompletedTransaction(lock.action['_rawdesttx'], false); await delay(7000); // Step 4: seller signs bid txn (full) and submits log(' >>>>> Step 4: seller signs bid txn (full) and submits'); - const complete = await seller.complete(ok, bid_stripped, accept_stripped, lock_stripped); - expect(complete).toBeCompletedTransaction(); + const complete = await seller.complete(WALLET, ok, bid_stripped, accept_stripped, lock_stripped); + await expectCompletedTransaction(complete, true); const completeTxid = await buyerNode0.sendRawTransaction(complete); expect(completeTxid).toBeDefined(); @@ -228,7 +269,7 @@ describe('Buyflow: mad ct', () => { expect(shouldFailToDestroy).toEqual(true); // Use daemon as a source of truth for what the current time is. - const now = (await buyerNode0.call('getblockchaininfo', []))['mediantime']; + const now = await buyerNode0.getBlockchainInfo().then(value => value.mediantime); const feasibleFrom = (now + 2880); // Travelling through time, 3000s in the future! @@ -244,20 +285,44 @@ describe('Buyflow: mad ct', () => { const destroytxid = await buyerNode0.sendRawTransaction(lock.action['_rawdesttx']); expect(destroytxid).toBeDefined(); + log('!!! buyflow destroy success!'); + }, 600000); + // tslint:disable:no-duplicated-branches + const expectCompletedTransaction = async (rawtx: any, complete = true) => { + const verify = await buyerNode0.verifyRawTransaction([rawtx]); + const completed = verify['complete']; + + if (complete && !completed) { + throw new Error('expected ' + rawtx + ' to be ' + + (complete ? 'completed' : 'incomplete') + + ', but received ' + completed + ' instead.'); + } else if (!complete && completed) { + throw new Error('expected ' + rawtx + ' to be ' + + (complete ? 'completed' : 'incomplete') + + ', but received ' + completed + ' instead.'); + } + }; + + const expectUtxoWithAmount = async (txid: string, node: Rpc, amount: number) => { + const outputs: RpcUnspentOutput[] = await node.listUnspent(WALLET, OutputType.ANON, 0); + + // log(outputs); - const delay = ms => { - return new Promise(resolve => { - return setTimeout(resolve, ms); + const found = outputs.find(utxo => { + log('find: ' + utxo.txid + ' === ' + txid + ', ' + utxo.amount + ' === ' + amount); + return (utxo.txid === txid && utxo.amount === amount); }); + if (!found) { + throw new Error(`expected ${txid} to be found on the node, but didn't find it.`); + } }; - // const expect2: any = Object.assign(expect); expect.extend({ async toBeCompletedTransaction(rawtx: any): Promise { - const verify = await buyerNode0.call('verifyrawtransaction', [rawtx]); + const verify = await buyerNode0.verifyRawTransaction([rawtx]); const completed = verify['complete']; if (completed) { return { @@ -277,7 +342,14 @@ describe('Buyflow: mad ct', () => { expect.extend({ async toBeUtxoWithAmount(txid: string, node: Rpc, amount: number): Promise { - const found = (await node.call('listunspentanon', [0])).find(utxo => (utxo.txid === txid && utxo.amount === amount)); + const outputs: RpcUnspentOutput[] = await node.listUnspent(WALLET, OutputType.ANON, 0); + + // log(outputs); + + const found = outputs.find(utxo => { + log('find: ' + utxo.txid + ' === ' + txid + ', ' + utxo.amount + ' === ' + amount); + return (utxo.txid === txid && utxo.amount === amount); + }); if (found) { return { message: () => @@ -295,7 +367,7 @@ describe('Buyflow: mad ct', () => { }); const timeTravel = (expectedUnixTime: number, node: Rpc) => { - return node.call('setmocktime', [expectedUnixTime, true]); + return node.call('setmocktime', [expectedUnixTime, true], WALLET); }; const waitTillJumped = async (expectedUnixTime: number, node: Rpc) => { @@ -303,9 +375,10 @@ describe('Buyflow: mad ct', () => { let wait = true; while (wait) { - const currentTime = (await node.call('getblockchaininfo', []))['mediantime']; + + const currentTime = await node.getBlockchainInfo().then(value => value.mediantime); wait = (currentTime <= expectedUnixTime); - // console.log(wait ? 'waiting..' : ('finished! ' + currentTime + ' > ' + expectedUnixTime )); + log(wait ? 'waiting..' : ('finished! ' + currentTime + ' > ' + expectedUnixTime )); await delay(1000); } @@ -313,4 +386,5 @@ describe('Buyflow: mad ct', () => { }); }; + }); diff --git a/__tests__/buyflow/multisig.test.ts b/__tests__/buyflow/multisig.test.ts index ddc5b6b7f..27fd1651d 100644 --- a/__tests__/buyflow/multisig.test.ts +++ b/__tests__/buyflow/multisig.test.ts @@ -2,8 +2,8 @@ import * from 'jest'; import { MPM, OpenMarketProtocol } from '../../src/omp'; import { Cryptocurrency } from '../../src/interfaces/crypto'; import { BidConfiguration } from '../../src/interfaces/configs'; -import { EscrowType } from '../../src/interfaces/omp-enums'; -import { toSatoshis, log, strip } from '../../src/util'; +import { EscrowType, MPAction } from '../../src/interfaces/omp-enums'; +import { toSatoshis, strip } from '../../src/util'; import { FV_MPA_BID } from '../../src/format-validators/mpa_bid'; import { FV_MPA_ACCEPT } from '../../src/format-validators/mpa_accept'; import { FV_MPA_LOCK } from '../../src/format-validators/mpa_lock'; @@ -12,6 +12,8 @@ import { CoreRpcService } from '../../test/rpc.stub'; describe('Buyflow: multisig', () => { + const WALLET = ''; // use the default wallet + const delay = ms => { return new Promise(resolve => { return setTimeout(resolve, ms); @@ -22,7 +24,7 @@ describe('Buyflow: multisig', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_LISTING_ADD", + "type": "${MPAction.MPA_LISTING_ADD}", "item": { "information": { "title": "a 6 month old dog", @@ -32,6 +34,10 @@ describe('Buyflow: multisig', () => { "Animals" ] }, + "seller": { + "address": "pVHUY9AYwNSjbX8f1d4fPgYCkNmZxMC25p", + "signature": "H7yN04IMrwbUgqFXT5Jzr5BPS5vpNrc9deKaY6jkCh0icM5Z3V5rtle/EkugQccw0vk/K6CReQ8sSSDo5W9Vl1I=" + }, "payment": { "type": "SALE", "escrow": { @@ -98,25 +104,25 @@ describe('Buyflow: multisig', () => { jest.setTimeout(40000); // Step 1: Buyer does bid - bid = strip(await buyer.bid(config, ok)); + bid = strip(await buyer.bid(WALLET, config, ok)); FV_MPA_BID.validate(bid); await delay(10000); // Step 2: seller accepts AND signs release tx // the seller always wants his money back - accept = strip(await seller.accept(ok, bid)); + accept = strip(await seller.accept(WALLET, ok, bid)); FV_MPA_ACCEPT.validate(accept); // Step 3: buyer locks and submits await delay(5000); - lock = await buyer.lock(ok, bid, accept); + lock = await buyer.lock(WALLET, ok, bid, accept); const bidtx = lock.action['_rawbidtx']; lock = strip(lock); FV_MPA_LOCK.validate(lock); await node0.sendRawTransaction(bidtx); // Step 4: buyer optionally releases - release = await buyer.release(ok, bid, accept); + release = await buyer.release(WALLET, ok, bid, accept); await node0.sendRawTransaction(release); expect(bid).toBeDefined(); @@ -132,21 +138,21 @@ describe('Buyflow: multisig', () => { let complete: string; jest.setTimeout(40000); - const bid = await buyer.bid(config, ok); + const bid = await buyer.bid(WALLET, config, ok); FV_MPA_BID.validate(bid); await delay(7000); - accept = await seller.accept(ok, bid); + accept = await seller.accept(WALLET, ok, bid); FV_MPA_ACCEPT.validate(accept); await delay(5000); - lock = await buyer.lock(ok, bid, accept); + lock = await buyer.lock(WALLET, ok, bid, accept); const bidtx = lock.action['_rawbidtx']; lock = strip(lock); FV_MPA_LOCK.validate(lock); await node0.sendRawTransaction(bidtx); - complete = await seller.refund(ok, bid, accept, lock); + complete = await seller.refund(WALLET, ok, bid, accept, lock); await delay(5000); await node0.sendRawTransaction(complete); diff --git a/__tests__/format-validators/escrow/madct.test.ts b/__tests__/format-validators/escrow/madct.test.ts index b1299b412..d65423257 100644 --- a/__tests__/format-validators/escrow/madct.test.ts +++ b/__tests__/format-validators/escrow/madct.test.ts @@ -4,14 +4,15 @@ import { FV_MPA_BID } from '../../../src/format-validators/mpa_bid'; import { hash } from '../../../src/hasher/hash'; import { FV_MPA_ACCEPT } from '../../../src/format-validators/mpa_accept'; import { FV_MPA_LOCK } from '../../../src/format-validators/mpa_lock'; -import { clone, log } from '../../../src/util'; +import { clone } from '../../../src/util'; +import { MPAction } from '../../../src/interfaces/omp-enums'; const validate = FV_MPA_BID.validate; const ok_bid = JSON.parse( `{ "version": "0.1.0.0", "action": { - "type": "MPA_BID", + "type": "${MPAction.MPA_BID}", "generated": ${+new Date().getTime()}, "item": "${hash('listing')}", "buyer": { @@ -138,7 +139,7 @@ const ok_accept = JSON.parse( `{ "version": "0.1.0.0", "action": { - "type": "MPA_ACCEPT", + "type": "${MPAction.MPA_ACCEPT}", "generated": ${+new Date().getTime()}, "bid": "${hash('bid')}", "seller": { @@ -342,7 +343,7 @@ const ok_lock = JSON.parse( `{ "version": "0.1.0.0", "action": { - "type": "MPA_LOCK", + "type": "${MPAction.MPA_LOCK}", "generated": ${+new Date().getTime()}, "bid": "${hash('bid')}", "buyer": { diff --git a/__tests__/format-validators/escrow/multisig.test.ts b/__tests__/format-validators/escrow/multisig.test.ts index b71cfefae..147eabb85 100644 --- a/__tests__/format-validators/escrow/multisig.test.ts +++ b/__tests__/format-validators/escrow/multisig.test.ts @@ -4,6 +4,7 @@ import { hash } from '../../../src/hasher/hash'; import { FV_MPA_ACCEPT } from '../../../src/format-validators/mpa_accept'; import { FV_MPA_LOCK } from '../../../src/format-validators/mpa_lock'; import { clone } from '../../../src/util'; +import { MPAction } from '../../../src/interfaces/omp-enums'; describe('Multisig', () => { @@ -15,7 +16,7 @@ describe('Multisig', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_BID", + "type": "${MPAction.MPA_BID}", "generated": ${+new Date().getTime()}, "item": "${hash('listing')}", "buyer": { @@ -52,7 +53,7 @@ describe('Multisig', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_ACCEPT", + "type": "${MPAction.MPA_ACCEPT}", "bid": "${hash('bid')}", "seller": { "payment": { @@ -92,7 +93,7 @@ describe('Multisig', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_LOCK", + "type": "${MPAction.MPA_LOCK}", "bid": "${hash('bid')}", "buyer": { "payment": { diff --git a/__tests__/format-validators/mpa.test.ts b/__tests__/format-validators/mpa.test.ts index 13b68d196..40cd68b9e 100644 --- a/__tests__/format-validators/mpa.test.ts +++ b/__tests__/format-validators/mpa.test.ts @@ -1,5 +1,6 @@ import * from 'jest'; import { FV_MPM } from '../../src/format-validators/mpm'; +import { MPAction } from '../../src/interfaces/omp-enums'; describe('format-validator: MPA', () => { @@ -12,9 +13,9 @@ describe('format-validator: MPA', () => { test('validate a complete action', () => { const success = JSON.parse( `{ - "version": "0.1.0.0", + "version": "0.3.0", "action": { - "type": "MPA_LISTING_ADD" + "type": "${MPAction.MPA_LISTING_ADD}" } }`); @@ -27,6 +28,78 @@ describe('format-validator: MPA', () => { expect(fail).toBe(false); }); + test('validate another MPA_LISTING_ADD', () => { + const success = JSON.parse( + `{ + "version": "2.4.0", + "action": { + "type": "${MPAction.MPA_LISTING_ADD}", + "generated": 1592210962204, + "item": { + "information": { + "title": "test01", + "shortDescription": "test01 summary", + "longDescription": "test01 long description that doesn't mean much", + "category": [ + "ROOT", + "Particl", + "Free Swag" + ], + "location": { + "country": "AU", + "address": null + }, + "shippingDestinations": [ + "AU", + "ZA", + "-US" + ] + }, + "seller": { + "address": "pZmMxcdzhqPknghTFHMRKyW4SGndXJw2H9", + "signature": "IHKl4Fdzk37S+6OS9/dVPKbTfCAlM+FNdmUxDxhqS4rKebcac4DiqxrnUG//75cqCBUchpeBNlshMfzvyCxGiyg=" + }, + "payment": { + "type": "SALE", + "escrow": { + "type": "MAD_CT", + "ratio": { + "buyer": 100, + "seller": 100 + }, + "secondsToLock": null, + "releaseType": "ANON" + }, + "options": [ + { + "currency": "PART", + "basePrice": 1, + "shippingPrice": { + "domestic": 0.1, + "international": 0.2 + }, + "address": { + "type": "STEALTH", + "address": "TetYrezhU9QHwdLzKq4Q3ajgj13STH8fxtzytWxT312LjmEwBVs7nBTYW6sjamAUfwLQWqGbzESXxF9jNwNnFnpjkhJ3PEKSncizDP" + } + } + ] + }, + "objects": [] + }, + "hash": "a90b35ef3d3a77ef2496bb00e6e5009e6267840add627341d79cae0241316a36" + } + }`); + + let fail: boolean; + try { + fail = !validate(success); + } catch (e) { + fail = true; + } + expect(fail).toBe(false); + }); + test('validate missing type', () => { const missing_type = JSON.parse( `{ @@ -48,7 +121,7 @@ describe('format-validator: MPA', () => { `{ "version": "", "action": { - "type": "MPA_LISTING_ADD" + "type": "${MPAction.MPA_LISTING_ADD}" } }`); let fail: boolean; @@ -64,7 +137,7 @@ describe('format-validator: MPA', () => { const missing_version = JSON.parse( `{ "action": { - "type": "MPA_LISTING_ADD" + "type": "${MPAction.MPA_LISTING_ADD}" } }`); let fail: boolean; diff --git a/__tests__/format-validators/mpa_accept.test.ts b/__tests__/format-validators/mpa_accept.test.ts index 8b37b2082..a72562364 100644 --- a/__tests__/format-validators/mpa_accept.test.ts +++ b/__tests__/format-validators/mpa_accept.test.ts @@ -2,6 +2,7 @@ import * from 'jest'; import { FV_MPA_ACCEPT } from '../../src/format-validators/mpa_accept'; import { hash } from '../../src/hasher/hash'; import { clone } from '../../src/util'; +import { MPAction } from '../../src/interfaces/omp-enums'; describe('format-validator: MPA_ACCEPT', () => { @@ -14,7 +15,7 @@ describe('format-validator: MPA_ACCEPT', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_ACCEPT", + "type": "${MPAction.MPA_ACCEPT}", "bid": "${hash('bid')}", "seller": { "payment": { diff --git a/__tests__/format-validators/mpa_bid.test.ts b/__tests__/format-validators/mpa_bid.test.ts index 85c5b19f4..6c6b14a71 100644 --- a/__tests__/format-validators/mpa_bid.test.ts +++ b/__tests__/format-validators/mpa_bid.test.ts @@ -2,6 +2,7 @@ import * from 'jest'; import { FV_MPA_BID } from '../../src/format-validators/mpa_bid'; import { hash } from '../../src/hasher/hash'; import { clone } from '../../src/util'; +import { MPAction } from '../../src/interfaces/omp-enums'; describe('format-validator: MPA_BID', () => { @@ -10,7 +11,7 @@ describe('format-validator: MPA_BID', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_BID", + "type": "${MPAction.MPA_BID}", "generated": ${+ new Date().getTime()}, "item": "${hash('listing')}", "buyer": { diff --git a/__tests__/format-validators/mpa_listing_add.test.ts b/__tests__/format-validators/mpa_listing_add.test.ts index 11d580771..9bb43c1d2 100644 --- a/__tests__/format-validators/mpa_listing_add.test.ts +++ b/__tests__/format-validators/mpa_listing_add.test.ts @@ -1,7 +1,8 @@ import * from 'jest'; import { FV_MPA_LISTING } from '../../src/format-validators/mpa_listing_add'; import { hash } from '../../src/hasher/hash'; -import { clone } from '../../src/util' +import { clone } from '../../src/util'; +import { MPAction } from '../../src/interfaces/omp-enums'; describe('format-validator: MPA_BID', () => { @@ -11,9 +12,13 @@ describe('format-validator: MPA_BID', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_LISTING_ADD", + "type": "${MPAction.MPA_LISTING_ADD}", "hash": "${hash('hash')}", "item": { + "seller": { + "address": "PASDF", + "signature": "Asdf" + }, "information": { "title": "a 6 month old dog", "shortDescription": "very cute", @@ -48,6 +53,68 @@ describe('format-validator: MPA_BID', () => { } }`); + const arnold = JSON.parse( + `{ + "version": "2.4.0", + "action": { + "type": "${MPAction.MPA_LISTING_ADD}", + "generated": 1592210962204, + "item": { + "information": { + "title": "test01", + "shortDescription": "test01 summary", + "longDescription": "test01 long description that doesn't mean much", + "category": [ + "ROOT", + "Particl", + "Free Swag" + ], + "location": { + "country": "AU", + "address": null + }, + "shippingDestinations": [ + "AU", + "ZA", + "-US" + ] + }, + "seller": { + "address": "pZmMxcdzhqPknghTFHMRKyW4SGndXJw2H9", + "signature": "IHKl4Fdzk37S+6OS9/dVPKbTfCAlM+FNdmUxDxhqS4rKebcac4DiqxrnUG//75cqCBUchpeBNlshMfzvyCxGiyg=" + }, + "payment": { + "type": "SALE", + "escrow": { + "type": "MAD_CT", + "ratio": { + "buyer": 100, + "seller": 100 + }, + "secondsToLock": null, + "releaseType": "ANON" + }, + "options": [ + { + "currency": "PART", + "basePrice": 1, + "shippingPrice": { + "domestic": 0.1, + "international": 0.2 + }, + "address": { + "type": "STEALTH", + "address": "TetYrezhU9QHwdLzKq4Q3ajgj13STH8fxtzytWxT312LjmEwBVs7nBTYW6sjamAUfwLQWqGbzESXxF9jNwNnFnpjkhJ3PEKSncizDP" + } + } + ] + }, + "objects": [] + }, + "hash": "a90b35ef3d3a77ef2496bb00e6e5009e6267840add627341d79cae0241316a36" + } + }`); + beforeAll(async () => { // }); @@ -64,6 +131,18 @@ describe('format-validator: MPA_BID', () => { expect(result).toBe(true); }); + test('should validate arnolds listing', () => { + + let result = false; + try { + result = validate(ok); + } catch (e) { + console.log(e); + result = true; + } + expect(result).toBe(true); + }); + test('should fail to validate a listing', () => { const horrible_fail = JSON.parse( `{ diff --git a/__tests__/format-validators/mpa_lock.test.ts b/__tests__/format-validators/mpa_lock.test.ts index 13cb9cd59..0319895f2 100644 --- a/__tests__/format-validators/mpa_lock.test.ts +++ b/__tests__/format-validators/mpa_lock.test.ts @@ -2,6 +2,7 @@ import * from 'jest'; import { FV_MPA_LOCK } from '../../src/format-validators/mpa_lock'; import { hash } from '../../src/hasher/hash'; import { clone } from '../../src/util'; +import { MPAction } from '../../src/interfaces/omp-enums'; describe('format-validator: MPA_LOCK', () => { @@ -10,7 +11,7 @@ describe('format-validator: MPA_LOCK', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_LOCK", + "type": "${MPAction.MPA_LOCK}", "bid": "${hash('bid')}", "buyer": { "payment": { diff --git a/__tests__/hash.test.ts b/__tests__/hash.test.ts index aedf69539..de356ce75 100644 --- a/__tests__/hash.test.ts +++ b/__tests__/hash.test.ts @@ -1,17 +1,141 @@ import * from 'jest'; -import { hash, deepSortObject, ConfigurableHasher } from '../src/hasher/hash'; -import { clone, strip, log } from '../src/util'; +import { hash, ConfigurableHasher } from '../src/hasher/hash'; +import { clone, strip } from '../src/util'; import { sha256 } from 'js-sha256'; import { HashableListingMessageConfig } from '../src/hasher/config/listingitemadd'; import { HashableValidator } from '../src/format-validators/hashable'; +import { MPAction } from '../src/interfaces/omp-enums'; +import { BaseHashableConfig, HashableFieldConfig, HashableFieldValueConfig } from '../src/interfaces/configs'; + +export class HashableImageCreateRequestConfig extends BaseHashableConfig { + public fields = [{ + from: 'data', + to: 'imageData' + }] as HashableFieldConfig[]; + + constructor(values?: HashableFieldValueConfig[]) { + super(values); + } +} describe('Hash', () => { + // A shrunken picture of a Japanese cat with a milk carton on his head, converted to base64 using the *NIX command `base64` + const milkcatSmall = '/9j/4AAQSkZJRgABAQEAoACgAAD/4RDpRXhpZgAASUkqAAgAAAAMAA4BAgAgAAAAngAAAA8BAgAYAAAAvgAAABABAgAQAAAA1gAA' + + 'ABIBAwABAAAAAQAAABoBBQABAAAA5gAAABsBBQABAAAA7gAAACgBAwABAAAAAgAAADEBAgAMAAAA9gAAADIBAgAUAAAAAgEAABMC' + + 'AwABAAAAAgAAAGmHBAABAAAAOAMAAKXEBwAiAgAAFgEAAMINAABPTFlNUFVTIERJR0lUQUwgQ0FNRVJBICAgICAgICAgAE9MWU1Q' + + 'VVMgSU1BR0lORyBDT1JQLiAgAEZFMzEwLFg4NDAsQzUzMACgAAAAAQAAAKAAAAABAAAAR0lNUCAyLjguMTYAMjAxODowMToxNSAy' + + 'MDo1NzoyMABQcmludElNADAzMDAAACUAAQAUABQAAgABAAAAAwDuAAAABwAAAAAACAAAAAAACQAAAAAACgAAAAAACwA2AQAADAAA' + + 'AAAADQAAAAAADgBOAQAAEAByAQAAIADGAQAAAAEDAAAAAQH/AAAAAgGDAAAAAwGDAAAABAGDAAAABQGDAAAABgGDAAAABwGAgIAA' + + 'EAGAAAAAAAIAAAAABwIAAAAACAIAAAAACQIAAAAACgIAAAAACwLoAQAADQIAAAAAIAIAAgAAAAMDAAAAAQP/AAAAAgODAAAAAwOD' + + 'AAAABgODAAAAEAOAAAAAAAQAAAAACREAABAnAAALDwAAECcAAJcFAAAQJwAAsAgAABAnAAABHAAAECcAAF4CAAAQJwAAiwAAABAn' + + 'AADLAwAAECcAAOUbAAAQJwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAABQUFAAAAQECAgMDA//8AAEBAgIDAwP//AABAQICAwMD//wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUFBQAA' + + 'AEBAgIDAwP//AABAQICAwMD//wAAQECAgMDA//8fAJqCBQABAAAAsgQAAJ2CBQABAAAAugQAACKIAwABAAAABQAAACeIAwABAAAA' + + 'UAAAAACQBwAEAAAAMDIyMQOQAgAUAAAAwgQAAASQAgAUAAAA1gQAAAGRBwAEAAAAAQIDAASSCgABAAAA6gQAAAWSBQABAAAA8gQA' + + 'AAeSAwABAAAAAgAAAAiSAwABAAAAAAAAAAmSAwABAAAAGAAAAAqSBQABAAAA+gQAAHySBwAcCAAAAgUAAIaSBwB9AAAAHg0AAACg' + + 'BwAEAAAAMDEwMAGgAwABAAAAAQAAAAKgBAABAAAAMgAAAAOgBAABAAAAJgAAAAWgBAABAAAApA0AAACjBwABAAAAAwAAAAGkAwAB' + + 'AAAAAAAAAAKkAwABAAAAAAAAAAOkAwABAAAAAAAAAASkBQABAAAAnA0AAAakAwABAAAAAAAAAAekAwABAAAAAQAAAAikAwABAAAA' + + 'AAAAAAmkAwABAAAAAAAAAAqkAwABAAAAAAAAAAAAAAABAAAAPAAAACEAAAAKAAAAMjAwOTowNjoxNCAxNjo0OToxOQAyMDA5OjA2' + + 'OjE0IDE2OjQ5OjE5AAAAAAAKAAAAKQEAAGQAAABsAgAAZAAAAE9MWU1QAAEAGQAEAQIABgAAADgGAAAAAgQAAwAAAD4GAAACAgMA' + + 'AQAAAAAAAAADAgMAAQAAAAAAAAAEAgUAAQAAAEoGAAAFAgUAAQAAAFIGAAAGAggABgAAAFoGAAAHAgIABgAAAGYGAAAJAgcAIAAA' + + 'AGwGAAAKAgQAAgAAAIwGAAALAgUAAQAAAJQGAAABBAMAAQAAAAEAAAACBAQAAQAAAAEQAAIDBAMAAQAAAAEAAAAABQMAAQAAAAAA' + + 'AAAgIAcANgAAAJwGAAAAIQcAuAAAANIGAAAAIgcAGgEAAIoHAAAAIwcA9gAAAKQIAAAAJAcAHgAAAJoJAAAAJQcAHgAAALgJAAAA' + + 'JgcA6gAAANYJAAAAJwcAQgAAAMAKAAAAKAcACgIAAAILAAAAKQcAEgAAAAwNAAAxLjAwMwAAAAAAAAAAAAAAAABkAAAAZAAAAEcc' + + 'AADoAwAASf/F/mD+Tf/N/m/+RDQzNjgAT0xZTVBVUyBESUdJVEFMIENBTUVSQSAgICAgICAgIAAAAAAAAAAAAAgAAAABAAAABAAA' + + 'AAcABAAAADAxMDAAAQQAAQAAAAAAAAABAQQAAQAAAAAAAAACAQQAAQAAAAAAAAAAAAAADgAAAQIACgAAAJgHAAABAQIAAwAAAE9L' + + 'AAACAQIAAwAAAE9LAAADAQIAAwAAAE9LAAAEAQIAAwAAAE9LAAARAQIAAwAAAE9LAAAGAQIAAwAAAE9LAAAIAQIAAwAAAE9LAAAP' + + 'AQIAAwAAAE9LAAAJAQMAAQAAAPYAAAAQAQMAAQAAAD4AAAAKAQMAAQAAAHMNAAAOAQMAAQAAAKIAAAASAQMAAQAAAO4CAAAAAAAA' + + 'MS4wMDMAhwEgEBcAAAIEAAEAAACtKAEAAQIEAAEAAAD0LAAAAgIBAAEAAAAAAAAAAwIDAAEAAAAdAAAABAIBAAEAAAABAAAABgIE' + + 'AAEAAAC+IgEABwIEAAEAAAByOgAACAIBAAEAAAAAAAAACQIDAAEAAAAcAQAACgIBAAEAAAAAAAAADAIDAAEAAABiAAAADQIDAAEA' + + 'AACBAAAADgIDAAEAAABkAAAADwIDAAEAAAB4AAAAFAIDAAEAAAAGAAAAFQIDAAEAAACAAAAAFwIDAAEAAACBAAAAGAIDAAEAAAAA' + + 'AAAAGQIDAAEAAABmAAAAGgIDAAEAAABwAAAAHwIBAAEAAAAAAAAAIgIBAAEAAAAAAAAAJQIDAAEAAACQAAAAAAAAABQAAAMBAAEA' + + 'AAAAAAAAAQMBAAEAAAAAAAAAAgMBAAEAAAAAAAAAAwMEAAEAAAAAAAAABAMDAAEAAACgAAAABQMDAAEAAAAFAQAACgMDAAEAAAAA' + + 'AAAADAMBAAEAAAAAAAAADQMBAAEAAAAAAAAADgMDAAEAAABcAAAADwMDAAEAAAAAAAAAEwMDAAEAAAAc/wAAFAMDAAEAAAAAAAAA' + + 'FQMDAAEAAAAAAAAAGAMDAAEAAABIQAAAIAMDAAEAAAC1DgAAIQMDAAEAAACyDgAAIgMDAAEAAAAAAAAAIwMDAAEAAABkAAAAJAMD' + + 'AAEAAAC8AgAAAAAAAAIAAAQBAAEAAAADAAAAAQQDAAEAAAC7DAAAAAAAAAIAAgUBAAEAAAAKAAAABAUDAAEAAAAAAAAAAAAAABMA' + + 'AAYEAAEAAACAreYAAQYEAAEAAADwKnwBAgYEAAEAAAAQfP8AAwYDAAEAAACHBgAABAYDAAEAAABhBgAABwYDAAEAAABWBAAACAYD' + + 'AAEAAACUCQAACQYDAAEAAAB5AgAACgYBAAEAAAAIAAAACwYDAAEAAAAABAAADAYDAAEAAAAABAAAEgYDAAEAAACrAQAAFAYDAAEA' + + 'AACfAQAAGgYDAAEAAAABAAAAHgYEAAEAAAAAAAAAHwYEAAEAAAAAAAAAIAYEAAEAAAAAAAAAKQYDAAEAAAAMCAAAKgYDAAEAAACb' + + 'BQAAAAAAAAUAAAcIAAEAAAAmAAAAAQcIAAEAAAD//gAAAgcIAAEAAADN/gAAAwcIAAEAAAABAAAABAcBAAEAAAACAAAAAAAAABcA' + + 'AQgBAAEAAAAAAAAAAggBAAEAAAAAAAAABAgDAAEAAAACAAAABQgJAAEAAAA6BQAABggIAAEAAAAoAAAABwgDAAEAAAAHAAAACwgI' + + 'AAEAAAAXBQAADAgIAAEAAACIBQAADQgDAAEAAAAGAAAADggEAAEAAAAKAAAADwgIAAEAAAA6BQAAEAgDAAEAAAC1AAAAEQgDAAEA' + + 'AACHAQAAEggDAAEAAABkAAAAFAgDAAEAAAA7BQAAFQgDAAEAAADPMwAAFggDAAEAAABMBAAAFwgDAAEAAAC+AAAAGAgDAAEAAAB4' + + 'AAAAIQgDAHgAAAA0DAAAHwgBAAEAAAABAAAAJwgIAAEAAAAnBQAAKAgIAAEAAACWYAAAAAAAADoNtA92ESIVuhlULs8zGii0GQAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAQkB' + + 'AAEAAAAAAAAAAAAAAEFTQ0lJAAAAICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgAGQAAABkAAAAAgAB' + + 'AAIABAAAAFI5OAACAAcABAAAADAxMDAAAAAABgADAQMAAQAAAAYAAAAaAQUAAQAAABAOAAAbAQUAAQAAABgOAAAoAQMAAQAAAAIA' + + 'AAABAgQAAQAAACAOAAACAgQAAQAAAMECAAAAAAAASAAAAAEAAABIAAAAAQAAAP/Y/+AAEEpGSUYAAQEAAAEAAQAA/9sAQwBQNzxG' + + 'PDJQRkFGWlVQX3jIgnhubnj1r7mRyP///////////////////////////////////////////////////9sAQwFVWlp4aXjrgoLr' + + '/////////////////////////////////////////////////////////////////////////8AAEQgAHAAmAwEiAAIRAQMRAf/E' + + 'AB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGh' + + 'CCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqS' + + 'k5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEB' + + 'AQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1Lw' + + 'FWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZ' + + 'mqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8AiKADG7FI' + + 'VBwA1O2nsR+VKFAx65pAWlG0YGKqzKWkPzcZqx5ikjGaiPJpsCEJk5zRUmB6UUAJSgE01acPWgBxJC8jBpuaOppO1AC5opDRQB//' + + '2f/hDDxodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvADw/eHBhY2tldCBiZWdpbj0n77u/JyBpZD0nVzVNME1wQ2VoaUh6cmVT' + + 'ek5UY3prYzlkJz8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0nYWRvYmU6bnM6bWV0YS8nPgo8cmRmOlJERiB4bWxuczpyZGY9J2h0dHA6' + + 'Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMnPgoKIDxyZGY6RGVzY3JpcHRpb24geG1sbnM6ZXhpZj0naHR0' + + 'cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8nPgogIDxleGlmOkltYWdlRGVzY3JpcHRpb24+T0xZTVBVUyBESUdJVEFMIENBTUVS' + + 'QSAgICAgICAgIDwvZXhpZjpJbWFnZURlc2NyaXB0aW9uPgogIDxleGlmOk1ha2U+T0xZTVBVUyBJTUFHSU5HIENPUlAuICA8L2V4' + + 'aWY6TWFrZT4KICA8ZXhpZjpNb2RlbD5GRTMxMCxYODQwLEM1MzA8L2V4aWY6TW9kZWw+CiAgPGV4aWY6T3JpZW50YXRpb24+VG9w' + + 'LWxlZnQ8L2V4aWY6T3JpZW50YXRpb24+CiAgPGV4aWY6WFJlc29sdXRpb24+MTYwPC9leGlmOlhSZXNvbHV0aW9uPgogIDxleGlm' + + 'OllSZXNvbHV0aW9uPjE2MDwvZXhpZjpZUmVzb2x1dGlvbj4KICA8ZXhpZjpSZXNvbHV0aW9uVW5pdD5JbmNoPC9leGlmOlJlc29s' + + 'dXRpb25Vbml0PgogIDxleGlmOlNvZnR3YXJlPjEuMCAgICAgICAgICAgICAgICAgICAgICAgICAgICA8L2V4aWY6U29mdHdhcmU+' + + 'CiAgPGV4aWY6RGF0ZVRpbWU+MjAwOTowNjoxNCAxNjo0OToxOTwvZXhpZjpEYXRlVGltZT4KICA8ZXhpZjpZQ2JDclBvc2l0aW9u' + + 'aW5nPkNvLXNpdGVkPC9leGlmOllDYkNyUG9zaXRpb25pbmc+CiAgPGV4aWY6UHJpbnRJbWFnZU1hdGNoaW5nPjU0NiBieXRlcyB1' + + 'bmRlZmluZWQgZGF0YTwvZXhpZjpQcmludEltYWdlTWF0Y2hpbmc+CiAgPGV4aWY6Q29tcHJlc3Npb24+SlBFRyBjb21wcmVzc2lv' + + 'bjwvZXhpZjpDb21wcmVzc2lvbj4KICA8ZXhpZjpYUmVzb2x1dGlvbj43MjwvZXhpZjpYUmVzb2x1dGlvbj4KICA8ZXhpZjpZUmVz' + + 'b2x1dGlvbj43MjwvZXhpZjpZUmVzb2x1dGlvbj4KICA8ZXhpZjpSZXNvbHV0aW9uVW5pdD5JbmNoPC9leGlmOlJlc29sdXRpb25V' + + 'bml0PgogIDxleGlmOkV4cG9zdXJlVGltZT4xLzYwIHNlYy48L2V4aWY6RXhwb3N1cmVUaW1lPgogIDxleGlmOkZOdW1iZXI+Zi8z' + + 'LjM8L2V4aWY6Rk51bWJlcj4KICA8ZXhpZjpFeHBvc3VyZVByb2dyYW0+Q3JlYXRpdmUgcHJvZ3JhbW1lIChiaWFzZWQgdG93YXJk' + + 'cyBkZXB0aCBvZiBmaWVsZCk8L2V4aWY6RXhwb3N1cmVQcm9ncmFtPgogIDxleGlmOklTT1NwZWVkUmF0aW5ncz4KICAgPHJkZjpT' + + 'ZXE+CiAgICA8cmRmOmxpPjgwPC9yZGY6bGk+CiAgIDwvcmRmOlNlcT4KICA8L2V4aWY6SVNPU3BlZWRSYXRpbmdzPgogIDxleGlm' + + 'OkV4aWZWZXJzaW9uPkV4aWYgVmVyc2lvbiAyLjIxPC9leGlmOkV4aWZWZXJzaW9uPgogIDxleGlmOkRhdGVUaW1lT3JpZ2luYWw+' + + 'MjAwOTowNjoxNCAxNjo0OToxOTwvZXhpZjpEYXRlVGltZU9yaWdpbmFsPgogIDxleGlmOkRhdGVUaW1lRGlnaXRpemVkPjIwMDk6' + + 'MDY6MTQgMTY6NDk6MTk8L2V4aWY6RGF0ZVRpbWVEaWdpdGl6ZWQ+CiAgPGV4aWY6Q29tcG9uZW50c0NvbmZpZ3VyYXRpb24+CiAg' + + 'IDxyZGY6U2VxPgogICAgPHJkZjpsaT5ZIENiIENyIC08L3JkZjpsaT4KICAgPC9yZGY6U2VxPgogIDwvZXhpZjpDb21wb25lbnRz' + + 'Q29uZmlndXJhdGlvbj4KICA8ZXhpZjpFeHBvc3VyZUJpYXNWYWx1ZT4wLjAwIEVWPC9leGlmOkV4cG9zdXJlQmlhc1ZhbHVlPgog' + + 'IDxleGlmOk1heEFwZXJ0dXJlVmFsdWU+Mi45NyBFViAoZi8yLjgpPC9leGlmOk1heEFwZXJ0dXJlVmFsdWU+CiAgPGV4aWY6TWV0' + + 'ZXJpbmdNb2RlPkNlbnRyZS13ZWlnaHRlZCBhdmVyYWdlPC9leGlmOk1ldGVyaW5nTW9kZT4KICA8ZXhpZjpMaWdodFNvdXJjZT5V' + + 'bmtub3duPC9leGlmOkxpZ2h0U291cmNlPgogIDxleGlmOkZsYXNoIHJkZjpwYXJzZVR5cGU9J1Jlc291cmNlJz4KICA8L2V4aWY6' + + 'Rmxhc2g+CiAgPGV4aWY6Rm9jYWxMZW5ndGg+Ni4yIG1tPC9leGlmOkZvY2FsTGVuZ3RoPgogIDxleGlmOk1ha2VyTm90ZT4yMDc2' + + 'IGJ5dGVzIHVuZGVmaW5lZCBkYXRhPC9leGlmOk1ha2VyTm90ZT4KICA8ZXhpZjpVc2VyQ29tbWVudD4gICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg' + + 'ICAgICAgICAgICAgICAgICAgICAgICAgICA8L2V4aWY6VXNlckNvbW1lbnQ+CiAgPGV4aWY6Rmxhc2hQaXhWZXJzaW9uPkZsYXNo' + + 'UGl4IFZlcnNpb24gMS4wPC9leGlmOkZsYXNoUGl4VmVyc2lvbj4KICA8ZXhpZjpDb2xvclNwYWNlPnNSR0I8L2V4aWY6Q29sb3JT' + + 'cGFjZT4KICA8ZXhpZjpQaXhlbFhEaW1lbnNpb24+MTI4MDwvZXhpZjpQaXhlbFhEaW1lbnNpb24+CiAgPGV4aWY6UGl4ZWxZRGlt' + + 'ZW5zaW9uPjk2MDwvZXhpZjpQaXhlbFlEaW1lbnNpb24+CiAgPGV4aWY6RmlsZVNvdXJjZT5EU0M8L2V4aWY6RmlsZVNvdXJjZT4K' + + 'ICA8ZXhpZjpDdXN0b21SZW5kZXJlZD5Ob3JtYWwgcHJvY2VzczwvZXhpZjpDdXN0b21SZW5kZXJlZD4KICA8ZXhpZjpFeHBvc3Vy' + + 'ZU1vZGU+QXV0byBleHBvc3VyZTwvZXhpZjpFeHBvc3VyZU1vZGU+CiAgPGV4aWY6V2hpdGVCYWxhbmNlPkF1dG8gd2hpdGUgYmFs' + + 'YW5jZTwvZXhpZjpXaGl0ZUJhbGFuY2U+CiAgPGV4aWY6RGlnaXRhbFpvb21SYXRpbz4xLjAwPC9leGlmOkRpZ2l0YWxab29tUmF0' + + 'aW8+CiAgPGV4aWY6U2NlbmVDYXB0dXJlVHlwZT5TdGFuZGFyZDwvZXhpZjpTY2VuZUNhcHR1cmVUeXBlPgogIDxleGlmOkdhaW5D' + + 'b250cm9sPkxvdyBnYWluIHVwPC9leGlmOkdhaW5Db250cm9sPgogIDxleGlmOkNvbnRyYXN0Pk5vcm1hbDwvZXhpZjpDb250cmFz' + + 'dD4KICA8ZXhpZjpTYXR1cmF0aW9uPk5vcm1hbDwvZXhpZjpTYXR1cmF0aW9uPgogIDxleGlmOlNoYXJwbmVzcz5Ob3JtYWw8L2V4' + + 'aWY6U2hhcnBuZXNzPgogIDxleGlmOkludGVyb3BlcmFiaWxpdHlJbmRleD5SOTg8L2V4aWY6SW50ZXJvcGVyYWJpbGl0eUluZGV4' + + 'PgogIDxleGlmOkludGVyb3BlcmFiaWxpdHlWZXJzaW9uPjAxMDA8L2V4aWY6SW50ZXJvcGVyYWJpbGl0eVZlcnNpb24+CiA8L3Jk' + + 'ZjpEZXNjcmlwdGlvbj4KCjwvcmRmOlJERj4KPC94OnhtcG1ldGE+Cjw/eHBhY2tldCBlbmQ9J3InPz4K/9sAQwBQNzxGPDJQRkFG' + + 'WlVQX3jIgnhubnj1r7mRyP///////////////////////////////////////////////////9sAQwFVWlp4aXjrgoLr////////' + + '/////////////////////////////////////////////////////////////////8IAEQgAJgAyAwEhAAIRAQMRAf/EABcAAQEB' + + 'AQAAAAAAAAAAAAAAAAABAgP/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/aAAwDAQACEAMQAAABwi9rOOVIo3uucKAaMgILYigzSwsA' + + '/8QAGRAAAgMBAAAAAAAAAAAAAAAAARACESBA/9oACAEBAAEFAqQCOY9X/8QAFBEBAAAAAAAAAAAAAAAAAAAAQP/aAAgBAwEBPwEn' + + '/8QAFBEBAAAAAAAAAAAAAAAAAAAAQP/aAAgBAgEBPwEn/8QAFBABAAAAAAAAAAAAAAAAAAAAUP/aAAgBAQAGPwIP/8QAHRAAAwAC' + + 'AgMAAAAAAAAAAAAAAAERICEQMUFhgf/aAAgBAQABPyEJP6bt8XSMjL7OzszyPbJgmV5bHlXj/9oADAMBAAIAAwAAABBrjuTACMzK' + + 'AMxA/8QAFhEBAQEAAAAAAAAAAAAAAAAAAQBA/9oACAEDAQE/EJMf/8QAFxEBAAMAAAAAAAAAAAAAAAAAAQAhQP/aAAgBAgEBPxCD' + + 'WP8A/8QAIBABAAMAAgICAwAAAAAAAAAAAQARITFREEEggWFxof/aAAgBAQABPxAQQQrywj35ZkHOIWwbI1BXHM/ESnuDVD9zILfU' + + '9poe5+03v+TOiFXhGGx2GlvPx9kAJRcaFJ9S5ctg8eAChlb4Nm9z/9k='; + const ok = JSON.parse( `{ "version": "0.1.0.0", "action": { - "type": "MPA_LISTING_ADD", + "type": "${MPAction.MPA_LISTING_ADD}", "item": { "information": { "title": "a 6 month old dog", @@ -176,17 +300,6 @@ describe('Hash', () => { ] }); - let output = 'one'; - let two = 'two'; - try { - output = ConfigurableHasher.hash(ok_full_img_data.action, config); - two = ConfigurableHasher.hash(ok_less_img_data.action, config); - // output = hashListing(ok_full_img_data); - // two = hashListing(ok_less_img_data); - } catch (e) { - console.log(e); - } - expect(output).toBe(two); }); @@ -285,4 +398,21 @@ describe('Hash', () => { expect(h).toEqual('bee405d40383879ca57d4eb24b9153b356c85ee6f4bc810b8bb1b67c1112c0bd'); }); + test('Should fail because missing imageData', () => { + expect.assertions(1); + try { + ConfigurableHasher.hash({fail: true}, new HashableImageCreateRequestConfig()); + } catch ( ex ) { + expect(ex).toEqual(new Error('imageData: missing')); + } + }); + + test('Should return hash for HashableImage', () => { + const h = ConfigurableHasher.hash({ + data: milkcatSmall + }, new HashableImageCreateRequestConfig()); + + expect(h).toBe('0844d47be9d6c06de3db0835696e2d03b2fc22bef07061590c33c080c80cfae0'); + }); + }); diff --git a/__tests__/rpc-ct.test.ts b/__tests__/rpc-ct.test.ts index c318094d7..45b324d31 100644 --- a/__tests__/rpc-ct.test.ts +++ b/__tests__/rpc-ct.test.ts @@ -6,6 +6,8 @@ import { CtCoreRpcService } from '../test/rpc-ct.stub'; describe('CtRpc', () => { + const WALLET = ''; // use the default wallet + let omp0: OpenMarketProtocol; let omp1: OpenMarketProtocol; let omp2: OpenMarketProtocol; @@ -37,12 +39,12 @@ describe('CtRpc', () => { }); test('stealth addresses', async () => { - const sx = await rpc0.getNewStealthAddressWithEphem(); + const sx = await rpc0.getNewStealthAddressWithEphem(WALLET); const stripped = clone(sx); delete stripped['pubKey']; - const sx2 = await rpc1.getPubkeyForStealthWithEphem(stripped); + const sx2 = await rpc1.getPubkeyForStealthWithEphem(WALLET, stripped); expect(sx).toEqual(sx2); }); }); diff --git a/__tests__/sequence-verifiers/verify.test.ts b/__tests__/sequence-verifiers/verify.test.ts index f77401a49..8b3417db3 100644 --- a/__tests__/sequence-verifiers/verify.test.ts +++ b/__tests__/sequence-verifiers/verify.test.ts @@ -4,15 +4,16 @@ import { Sequence } from '../../src/sequence-verifier/verify'; import { clone } from '../../src/util'; import { HashableListingMessageConfig } from '../../src/hasher/config/listingitemadd'; import { HashableBidMessageConfig } from '../../src/hasher/config/bid'; +import { MPAction } from '../../src/interfaces/omp-enums'; describe('SequenceValidator', () => { const validate = Sequence.validate; const listing_ok = JSON.parse( `{ - "version": "0.1.0.0", + "version": "0.3.0", "action": { - "type": "MPA_LISTING_ADD", + "type": "${MPAction.MPA_LISTING_ADD}", "item": { "information": { "title": "a 6 month old dog", @@ -22,6 +23,10 @@ describe('SequenceValidator', () => { "Animals" ] }, + "seller": { + "address": "pVHUY9AYwNSjbX8f1d4fPgYCkNmZxMC25p", + "signature": "H7yN04IMrwbUgqFXT5Jzr5BPS5vpNrc9deKaY6jkCh0icM5Z3V5rtle/EkugQccw0vk/K6CReQ8sSSDo5W9Vl1I=" + }, "payment": { "type": "SALE", "escrow": { @@ -56,7 +61,7 @@ describe('SequenceValidator', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_BID", + "type": "${MPAction.MPA_BID}", "generated": ${+new Date().getTime()}, "item": "${hashedListing}", "buyer": { @@ -96,7 +101,7 @@ describe('SequenceValidator', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_ACCEPT", + "type": "${MPAction.MPA_ACCEPT}", "bid": "${hashedBid}", "seller": { "payment": { @@ -137,7 +142,7 @@ describe('SequenceValidator', () => { `{ "version": "0.1.0.0", "action": { - "type": "MPA_LOCK", + "type": "${MPAction.MPA_LOCK}", "bid": "${hashedBid}", "buyer": { "payment": { @@ -253,6 +258,6 @@ describe('SequenceValidator', () => { } catch (e) { error = e.toString(); } - expect(error).toEqual(expect.stringContaining('currency provided by MPA_BID not accepted by the listing')); + expect(error).toEqual(expect.stringContaining('currency provided by ' + MPAction.MPA_BID + ' not accepted by the listing')); }); }); diff --git a/package.json b/package.json index 17131c67f..35ac71622 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omp-lib", - "version": "0.1.112", + "version": "0.1.152", "description": "Particl OMP-library", "main": "dist/omp.js", "types": "dist/omp.d.js", @@ -14,22 +14,23 @@ "pretest": "tslint --project ./tsconfig.json --config ./tslint.json -t stylish '__tests__/**/*.{ts,tsx}'", "test": "npm-run-all -s lint test:run", "test:setup": "python3 particl-test-env.py", - "test:run": "jest --verbose", - "test:single": "npm run test:run -- --testPathPattern=$TEST -i", + "test:run": "jest --verbose --runInBand --detectOpenHandles", + "test:single": "npm run test:run -- --testPathPattern=$TEST", "lint": "tslint --project ./tsconfig.json --config ./tslint.json -t stylish 'src/**/*.{ts,tsx}'", "lint:fix": "tslint --fix --project ./tsconfig.json --config ./tslint.json -t stylish 'src/**/*.{ts,tsx}'" }, - "contributors": [ - { - "name": "Kewde", - "email": "kewde@particl.io", - "url": "https://github.com/kewde" - }, - { - "name": "Cube", - "email": "cube@particl.io" - } - ], + "contributors": [{ + "name": "Kewde", + "email": "kewde@particl.io", + "url": "https://github.com/kewde" + }, { + "name": "ludx", + "email": "ludx@particl.io", + "url": "https://github.com/xludx" + }, { + "name": "Cube", + "email": "cube@particl.io" + }], "license": "MIT", "dependencies": { "inversify": "^4.13.0", @@ -38,7 +39,7 @@ "particl-bitcore-lib": "https://github.com/kewde/particl-bitcore-lib.git", "pjson": "^1.0.9", "reflect-metadata": "^0.1.12", - "semverv": "^5.6.4", + "semverv": "^5.6.5", "tslib": "^1.9.3", "web-request": "^1.0.7" }, @@ -46,6 +47,7 @@ "@types/jest": "^23.1.4", "@types/node": "^10.5.2", "@types/semver": "^5.5.0", + "delay": "^4.3.0", "jest": "^23.6.0", "jest-cli": "^23.6.0", "jest-sonar-reporter": "^2.0.0", diff --git a/src/abstract/omp.ts b/src/abstract/omp.ts index c60c8f3bf..af68e836d 100644 --- a/src/abstract/omp.ts +++ b/src/abstract/omp.ts @@ -3,12 +3,12 @@ import { MPM } from '../interfaces/omp'; // This interface does do sanity checks first. export interface OMP { - bid(config: BidConfiguration, listing: MPM): Promise; - accept(listing: MPM, bid: MPM): Promise; - lock(listing: MPM, bid: MPM, lock: MPM): Promise; + bid(wallet: string, config: BidConfiguration, listing: MPM): Promise; + accept(wallet: string, listing: MPM, bid: MPM): Promise; + lock(wallet: string, listing: MPM, bid: MPM, lock: MPM): Promise; // complete() is only used in MADCT because the destroy tx needs to be finalized // and shared between all parties before the bid transaction is finalized. - complete(listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise; - release(listing: MPM, bid: MPM, accept: MPM): Promise; // TODO: add release signatures for seller in accept. - refund(listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise; // TODO: add refund signatures for seller in lock. + complete(wallet: string, listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise; + release(wallet: string, listing: MPM, bid: MPM, accept: MPM): Promise; // TODO: add release signatures for seller in accept. + refund(wallet: string, listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise; // TODO: add refund signatures for seller in lock. } diff --git a/src/abstract/rpc.ts b/src/abstract/rpc.ts index fc41aec73..f7252da52 100644 --- a/src/abstract/rpc.ts +++ b/src/abstract/rpc.ts @@ -1,82 +1,63 @@ import * as _ from 'lodash'; -import { BlindPrevout, CryptoAddress, Cryptocurrency, EphemeralKey, ISignature, OutputType, Prevout, ToBeBlindOutput } from '../interfaces/crypto'; +import { + BlindPrevout, + CryptoAddress, + Cryptocurrency, + EphemeralKey, + ISignature, + OutputType, + Prevout, + ToBeBlindOutput, + CryptoAddressType +} from '../interfaces/crypto'; import { TransactionBuilder } from '../transaction-builder/transaction'; -import { clone, fromSatoshis, toSatoshis } from '../util'; -import { RpcAddressInfo, RpcOutput, RpcRawTx, RpcUnspentOutput, RpcVout, RpcWallet, RpcWalletDir } from '../interfaces/rpc'; +import { clone, fromSatoshis, isObject, isString, isStringAndEnumValue, toSatoshis } from '../util'; +import { + RpcBlockchainInfo, + RpcAddressInfo, + RpcBlindInput, RpcBlindOrFeeBase, + RpcBlindSendToOutput, + RpcOutput, + RpcRawTx, + RpcUnspentOutput, + RpcVout, + RpcWallet, + RpcWalletDir +} from '../interfaces/rpc'; import { ConfidentialTransactionBuilder } from '../transaction-builder/confidential-transaction'; import { randomBytes } from 'crypto'; // tslint:disable max-classes-per-file bool-param-default -export interface RpcBlindInput extends RpcOutput { - type: string; - scriptPubKey: string; - amount_commitment: string; - blindingfactor: string; - redeemScript?: string; - sequence?: number; -} - -export interface RpcBlindOrFeeBase { - -} - -export interface RpcBlindOutput extends RpcBlindOrFeeBase { - rangeproof_params: any; - pubkey?: string; - ephemeral_key?: string; - script?: string; - nonce?: string; - data?: any; - address?: string; - amount?: number; - data_ct_fee?: number; -} - -export interface RpcCtFeeOutput extends RpcBlindOrFeeBase { - type: string; - amount: number; - data_ct_fee: number; -} - -export interface RpcBlindSendToOutput { - address: string; - amount: number; - blindingfactor: string; -} - /** * The abstract class for the Rpc class. */ export abstract class Rpc { - public abstract async call(method: string, params: any[]): Promise; -/* - public abstract async createWallet(name: string, disablePrivateKeys: boolean, blank: boolean): Promise; - public abstract async loadWallet(name: string): Promise; - public abstract async smsgSetWallet(name?: string): Promise; - public abstract async listLoadedWallets(): Promise; - public abstract async listWalletDir(): Promise; -*/ - public abstract async getNewAddress(): Promise; // returns address - public abstract async getAddressInfo(address: string): Promise; - public abstract async sendToAddress(address: string, amount: number, comment: string): Promise; // returns txid - public abstract async listUnspent(type: OutputType, minconf: number): Promise; - public abstract async lockUnspent(unlock: boolean, outputs: RpcOutput[], permanent: boolean): Promise; // successful - public abstract async importAddress(address: string, label: string, rescan: boolean, p2sh: boolean): Promise; // returns nothing - public abstract async createSignatureWithWallet(hex: string, prevtx: RpcOutput, address: string): Promise; // signature + public abstract async call(method: string, params: any[], wallet: string): Promise; + public abstract async getNewAddress(wallet: string): Promise; // returns address + public abstract async getAddressInfo(wallet: string, address: string): Promise; + public abstract async listUnspent(wallet: string, type: OutputType, minconf: number): Promise; + public abstract async sendToAddress(wallet: string, address: string, amount: number, comment: string): Promise; // returns txid + public abstract async lockUnspent(wallet: string, unlock: boolean, outputs: RpcOutput[], permanent: boolean): Promise; // successful + public abstract async importAddress(wallet: string, address: string, label: string, rescan: boolean, p2sh: boolean): Promise; // returns nothing + public abstract async createSignatureWithWallet(wallet: string, hex: string, prevtx: RpcOutput, address: string): Promise; // signature public abstract async getRawTransaction(txid: string, verbose?: boolean): Promise; public abstract async sendRawTransaction(rawtx: string): Promise; // returns txid - public async getNewPubkey(): Promise { - const address = await this.getNewAddress(); - return (await this.getAddressInfo(address)).pubkey; + public abstract async getBlockchainInfo(): Promise; + public abstract async verifyRawTransaction(): Promise; // TODO: result + + public async getNewPubkey(wallet: string): Promise { + const address = await this.getNewAddress(wallet); + const addressInfo: RpcAddressInfo = await this.getAddressInfo(wallet, address); + return addressInfo.pubkey; } // TODO: refactor this, cognitive-complexity 39 // TODO: use getPrevouts() from the CtRpc? // tslint:disable:cognitive-complexity - public async getNormalPrevouts(reqSatoshis: number): Promise { + public async getNormalPrevouts(wallet: string, reqSatoshis: number): Promise { const chosen: Prevout[] = []; // const utxoLessThanReq: number[] = []; let exactMatchIdx = -1; @@ -84,7 +65,7 @@ export abstract class Rpc { let chosenSatoshis = 0; const defaultIdxs: number[] = []; - const unspent: RpcUnspentOutput[] = await this.listUnspent(OutputType.PART, 0); + const unspent: RpcUnspentOutput[] = await this.listUnspent(wallet, OutputType.PART, 0); const filtered = unspent.filter( (output: RpcUnspentOutput, outIdx: number) => { @@ -142,8 +123,11 @@ export abstract class Rpc { // ... Step 3: If no summed values found, attempt to split a large enough output. if (utxoIdxs.length === 0 && maxOutputIdx !== -1 && toSatoshis(unspent[maxOutputIdx].amount) > reqSatoshis) { - const newAddr = await this.getNewAddress(); - const txid: string = await this.sendToAddress(newAddr, fromSatoshis(reqSatoshis), 'Split output'); + const newAddr = await this.getNewAddress(wallet); + const txid: string = await this.sendToAddress(wallet, newAddr, fromSatoshis(reqSatoshis), 'Split output'); + if (!txid) { + throw new Error('Send failed!'); + } const txData: RpcRawTx = await this.getRawTransaction(txid); const outData: RpcVout | undefined = txData.vout.find(outObj => outObj.valueSat === reqSatoshis); if (outData) { @@ -184,7 +168,7 @@ export abstract class Rpc { }); // tslint:enable:no-for-each-push - const success: boolean = await this.lockUnspent(false, chosen, true); + const success: boolean = await this.lockUnspent(wallet, false, chosen, true); if (!success) { throw new Error('Locking of unspent Outputs failed.'); } @@ -194,11 +178,12 @@ export abstract class Rpc { /** * Fetch the rawtx and set the utxo._satoshis and utxo._scriptPubKey to the tx's matching vout's valueSat * + * @param wallet * @param utxo */ public async loadTrustedFieldsForUtxos(utxo: Prevout): Promise { - const vout: RpcVout | undefined = (await this.getRawTransaction(utxo.txid)) - .vout.find((value: RpcVout) => value.n === utxo.vout); + const rawTx: RpcRawTx = await this.getRawTransaction(utxo.txid); + const vout: RpcVout | undefined = rawTx.vout.find((value: RpcVout) => value.n === utxo.vout); if (!vout || !vout.valueSat || !vout.scriptPubKey @@ -212,16 +197,17 @@ export abstract class Rpc { return utxo; } - public async importRedeemScript(script: string): Promise { - await this.importAddress(script, '', false, true); + public async importRedeemScript(wallet: string, script: string): Promise { + await this.importAddress(wallet, script, '', false, true); } /** * Sign a transaction and returns the signatures for an array of normal inputs. + * @param wallet * @param tx the transaction to build and sign for. * @param inputs the _normal_ inputs to sign for. */ - public async signRawTransactionForInputs(tx: TransactionBuilder, inputs: Prevout[]): Promise { + public async signRawTransactionForInputs(wallet: string, tx: TransactionBuilder, inputs: Prevout[]): Promise { const r: ISignature[] = []; // needs to synchronize, because the order needs to match the inputs order. @@ -248,8 +234,8 @@ export abstract class Rpc { safe: true // ts lint }; - const signature: string = await this.createSignatureWithWallet(hex, prevtx, input._address); - const pubKey: string = (await this.getAddressInfo(input._address)).pubkey; + const signature: string = await this.createSignatureWithWallet(wallet, hex, prevtx, input._address); + const pubKey: string = (await this.getAddressInfo(wallet, input._address)).pubkey; const sig = { signature, pubKey @@ -271,36 +257,64 @@ export abstract class CtRpc extends Rpc { // example implementations in test/rpc.stub.ts and rpc-ct.stub.ts // WALLET - generating keys, addresses. - public abstract async getNewStealthAddress(): Promise; - public abstract async sendTypeTo(typeIn: OutputType, typeOut: OutputType, outputs: RpcBlindSendToOutput[]): Promise; - - // Retrieving information of prevouts - // public abstract async listUnspentBlind(minconf: number): Promise; - // public abstract async listUnspentAnon(minconf: number): Promise; + public abstract async getNewStealthAddress(wallet: string, params?: any[]): Promise; + public abstract async sendTypeTo(wallet: string, typeIn: OutputType, typeOut: OutputType, outputs: RpcBlindSendToOutput[]): Promise; // public abstract async getBlindPrevouts(type: string, satoshis: number, blind?: string): Promise; - public abstract async getPrevouts(typeIn: OutputType, typeOut: OutputType, satoshis: number, blind?: string): Promise; - - // public abstract async getLastMatchingBlindFactor(prevouts: Prevout[] | ToBeBlindOutput[], outputs: ToBeBlindOutput[]): Promise; + public abstract async getPrevouts(wallet: string, typeIn: OutputType, typeOut: OutputType, satoshis: number, blind?: string): Promise; // Importing and signing - // public abstract async signRawTransactionForBlindInputs(tx: TransactionBuilder, inputs: BlindPrevout[], sx?: CryptoAddress): Promise; - public abstract async verifyCommitment(commitment: string, blindFactor: string, amount: number): Promise; + public abstract async verifyCommitment(wallet: string, commitment: string, blindFactor: string, amount: number): Promise; + + public async createPrevoutFrom( + wallet: string, typeFrom: OutputType, typeTo: OutputType, satoshis: number, blindingfactor?: string, address?: CryptoAddress + ): Promise { + if (wallet === undefined) { + throw new Error('Missing wallet. (createPrevoutFrom)'); + } - public abstract async createRawTransaction(inputs: BlindPrevout[], outputs: any[]): Promise; + console.log('OMP_LIB: createPrevoutFrom() createPrevoutFrom, wallet: ' + wallet); - public async createPrevoutFrom(typeFrom: OutputType, typeTo: OutputType, satoshis: number, blindingfactor?: string): Promise { let prevout: BlindPrevout; - const sx = await this.getNewStealthAddress(); + let sx: CryptoAddress; + + if (address && isObject(address) && isStringAndEnumValue(address.type, CryptoAddressType) && isString(address.address)) { + sx = address; + } else { + sx = await this.getNewStealthAddress(wallet); + if (!isObject(sx) || !isStringAndEnumValue(sx.type, CryptoAddressType) || !isString(sx.address)) { + throw new Error('Missing address. (createPrevoutFrom)'); + } + } const amount = fromSatoshis(satoshis); if (!blindingfactor) { blindingfactor = this.getRandomBlindFactor(); } - const txid = await this.sendTypeTo(typeFrom, typeTo, [{ address: sx.address, amount, blindingfactor}]); + const txid = await this.sendTypeTo(wallet, typeFrom, typeTo, [{ address: sx.address, amount, blindingfactor}]).catch(err => { + if (err) { + throw err; + } + return ''; + }); + if (!txid) { + throw new Error('Send failed!'); + } - const unspent: RpcUnspentOutput[] = await this.listUnspent(typeTo, 0); + // TODO: not sure if this is a bug in core or not, but it is not guaranteed that the output exists right after sendTypeTo + // 2020-07-08T09:39:15.197Z - debug: [CoreRpcService] call: sendtypeto ["anon" "blind" [{"address":"Tetxxx" "amount":0.9158134 "blindingfactor":"xxx"}]] + // 2020-07-08T09:39:15.360Z - debug: [CoreRpcService] txid: 0=[8bc2db72eb650c1cb328d83912ed25fce1452c104758c1f03d7b4d5cca479e5c] + // 2020-07-08T09:39:15.361Z - debug: [CoreRpcService] call: listunspentblind [0 9999999] + // 2020-07-08T09:39:15.364Z - debug: [ZmqWorker] ZMQ: receive(hashtx): 0=[8bc2db72eb650c1cb328d83912ed25fce1452c104758c1f03d7b4d5cca479e5c] + // Error: Not enough inputs! + + // TODO: doing something retarted here, this needs to be fixed!!! + await this.getRawTransaction(txid).catch(reason => console.log('OMP_LIB: createPrevoutFrom() txid: ' + txid + ' not available yet.')); + await this.getRawTransaction(txid).catch(reason => console.log('OMP_LIB: createPrevoutFrom() txid: ' + txid + ' not available yet.')); + await this.getRawTransaction(txid).catch(reason => console.log('OMP_LIB: createPrevoutFrom() txid: ' + txid + ' not available yet.')); + + const unspent: RpcUnspentOutput[] = await this.listUnspent(wallet, typeTo, 0); const found = unspent.find(tmpVout => { return (tmpVout.txid === txid && tmpVout.amount === fromSatoshis(satoshis)); }); @@ -329,7 +343,7 @@ export abstract class CtRpc extends Rpc { } as BlindPrevout; // Permanently lock the unspent output - await this.lockUnspent(false, [prevout], true); + await this.lockUnspent(wallet, false, [prevout], true); return prevout; } @@ -337,9 +351,10 @@ export abstract class CtRpc extends Rpc { /** * Load a set of trusted fields for a blind (u)txo. * also validates the satoshis entered in the utxo against the commitment! + * @param wallet * @param utxo the output to load the fields for. */ - public async loadTrustedFieldsForBlindUtxo(utxo: BlindPrevout): Promise { + public async loadTrustedFieldsForBlindUtxo(wallet: string, utxo: BlindPrevout): Promise { const tx: RpcRawTx = await this.getRawTransaction(utxo.txid); const found: RpcVout | undefined = tx.vout.find(i => i.n === utxo.vout); @@ -368,7 +383,7 @@ export abstract class CtRpc extends Rpc { const scriptPubKey = found.scriptPubKey.hex; const address = found.scriptPubKey.addresses[0]; - const ok = await this.verifyCommitment(commitment, utxo.blindFactor, utxo._satoshis); + const ok = await this.verifyCommitment(wallet, commitment, utxo.blindFactor, utxo._satoshis); if (!ok) { console.error('Commitment is not matching amount or blindfactor.'); throw new Error('Commitment is not matching amount or blindfactor.'); @@ -380,7 +395,7 @@ export abstract class CtRpc extends Rpc { return utxo; } - public async generateRawConfidentialTx(inputs: any[], outputs: ToBeBlindOutput[], feeSatoshis: number): Promise { + public async generateRawConfidentialTx(wallet: string, inputs: BlindPrevout[], outputs: ToBeBlindOutput[], feeSatoshis: number): Promise { // Already a cloned object, rename fields to fit createrawparttransaction // inputs.forEach((prevout: any) => { prevout.amount_commitment = prevout._commitment; prevout.blindingfactor = prevout.blindFactor; }); @@ -401,7 +416,7 @@ export abstract class CtRpc extends Rpc { scriptPubKey: prevout._scriptPubKey, amount_commitment: prevout._commitment, // fromSatoshis(prevout._satoshis), blindingfactor: prevout.blindFactor - }; + } as RpcBlindInput; if (prevout._redeemScript) { ( i).redeemScript = prevout._redeemScript; @@ -468,20 +483,20 @@ export abstract class CtRpc extends Rpc { amount: 0 } as RpcBlindOrFeeBase].concat(outs); - const tx = (await this.call('createrawparttransaction', [inps, outs])); + const tx = (await this.call('createrawparttransaction', [inps, outs], wallet)); const rawtx = tx.hex; return rawtx; } - public async getNewStealthAddressWithEphem(sx?: CryptoAddress): Promise { + public async getNewStealthAddressWithEphem(wallet: string, sx?: CryptoAddress): Promise { if (!sx) { - sx = await this.getNewStealthAddress(); + sx = await this.getNewStealthAddress(wallet); } else { sx = clone(sx) as CryptoAddress; } - const info = await this.call('derivefromstealthaddress', [sx.address]); + const info = await this.call('derivefromstealthaddress', [sx.address], wallet); sx.pubKey = info.pubkey; sx.ephem = { public: info.ephemeral_pubkey, @@ -490,12 +505,12 @@ export abstract class CtRpc extends Rpc { return sx; } - public async getPubkeyForStealthWithEphem(sx: CryptoAddress): Promise { + public async getPubkeyForStealthWithEphem(wallet: string, sx: CryptoAddress): Promise { if (!sx.ephem) { throw new Error('Missing EphemeralKey.'); } - const info = await this.call('derivefromstealthaddress', [sx.address, sx.ephem.private]); + const info = await this.call('derivefromstealthaddress', [sx.address, sx.ephem.private], wallet); sx.pubKey = info.pubkey; return sx; } @@ -504,7 +519,7 @@ export abstract class CtRpc extends Rpc { return randomBytes(32).toString('hex'); } - public async getLastMatchingBlindFactor(inputs: (Array<{ blindFactor: string; }>), outputs: ToBeBlindOutput[]): Promise { + public async getLastMatchingBlindFactor(wallet: string, inputs: (Array<{ blindFactor: string; }>), outputs: ToBeBlindOutput[]): Promise { const inp = inputs.map(i => i.blindFactor); let out = outputs.map(i => i.blindFactor); @@ -518,72 +533,88 @@ export abstract class CtRpc extends Rpc { out.push(blindFactor); inp.push(blindFactor); } - const b = (await this.call('generatematchingblindfactor', [inp, out])).blind; + const b = (await this.call('generatematchingblindfactor', [inp, out], wallet)).blind; // console.log('generated b =', b); return b; } - public async signRawTransactionForBlindInputs(tx: ConfidentialTransactionBuilder, inputs: BlindPrevout[], sx?: CryptoAddress): Promise { + public async signRawTransactionForBlindInput(wallet: string, tx: ConfidentialTransactionBuilder, + input: BlindPrevout, + sx?: CryptoAddress): Promise { + + let sig; + + // If not a stealth address, sign with key in wallet. + if (!sx) { + + // TODO: this.createSignatureWithWallet exists + const txhex = tx.build(); + const prevtxn = { + txid: input.txid, + vout: input.vout, + scriptPubKey: input._scriptPubKey!, + amount_commitment: input._commitment + }; + + const addressInfo: RpcAddressInfo = await this.getAddressInfo(wallet, input._address!); + const signature = await this.createSignatureWithWallet(wallet, txhex, prevtxn, input._address!); + const pubKey: string = addressInfo.pubkey; + sig = { + signature, + pubKey + }; + + } else { + // If it's a stealth address, derive the key and sign for it. + // TODO: verify sx type + if (!sx.ephem || !sx.ephem.public) { + throw new Error('Missing ephemeral (public key) for stealth address.'); + } + const derived = (await this.call('derivefromstealthaddress', [sx.address, sx.ephem.public], wallet)); + const params = [ + tx.build(), + { + txid: input.txid, + vout: input.vout, + scriptPubKey: input._scriptPubKey, + amount_commitment: input._commitment, + redeemScript: input._redeemScript + }, + derived.privatekey + ]; + + sig = { + signature: (await this.call('createsignaturewithkey', params, wallet)), + pubKey: derived.pubkey + }; + } + + // TODO: why isnt this done in the previous if/else? + // If a stealth address is provided, we assume that were signing/spending + // from a bid txn with a more complicated witness stack (cfr puzzleWitness()) + if (!sx) { + tx.setWitness(input, sig); + } + + return sig; + } + + public async signRawTransactionForBlindInputs(wallet: string, tx: ConfidentialTransactionBuilder, + inputs: BlindPrevout[], + sx?: CryptoAddress): Promise { const r: ISignature[] = []; // log("signing for rawtx with blind innputs" + tx.txid) // needs to synchronize, because the order needs to match // the inputs order. for (const input of inputs) { - let sig; - - // If not a stealth address, sign with key in wallet. - if (!sx) { - const params = [ - tx.build(), - { - txid: input.txid, - vout: input.vout, - scriptPubKey: input._scriptPubKey, - amount_commitment: input._commitment - }, - input._address - ]; - - sig = { - signature: (await this.call('createsignaturewithwallet', params)), - pubKey: (await this.call('getaddressinfo', [input._address])).pubkey - }; - } else { - // If it's a stealth address, derive the key and sign for it. - // TODO: verify sx type - if (!sx.ephem || !sx.ephem.public) { - throw new Error('Missing ephemeral (public key) for stealth address.'); - } - const derived = (await this.call('derivefromstealthaddress', [sx.address, sx.ephem.public])); - const params = [ - tx.build(), - { - txid: input.txid, - vout: input.vout, - scriptPubKey: input._scriptPubKey, - amount_commitment: input._commitment, - redeemScript: input._redeemScript - }, - derived.privatekey - ]; - - sig = { - signature: (await this.call('createsignaturewithkey', params)), - pubKey: derived.pubkey - }; - } + // separated into its own function to get rid of the need to use an index in MadCtBuilder.complete() + const sig = await this.signRawTransactionForBlindInput(wallet, tx, input, sx); r.push(sig); // console.log('signRawTransactionForBlindInputs(): txid= ', tx.txid); - // console.log('signRawTransactionForBlindInputs(): signing for ', input) - // console.log('signRawTransactionForBlindInputs(): sig ', sig) - - // If a stealth address is provided, we assume that were signing/spending - // from a bid txn with a more complicated witness stack (cfr puzzleWitness()) - if (!sx) { - tx.setWitness(input, sig); - } + // console.log('signRawTransactionForBlindInputs(): signing for ', input); + // console.log('signRawTransactionForBlindInputs(): sig ', sig); } return r; diff --git a/src/abstract/transactions.ts b/src/abstract/transactions.ts index 38ee7f250..a19ef7460 100644 --- a/src/abstract/transactions.ts +++ b/src/abstract/transactions.ts @@ -1,19 +1,19 @@ import { MPA } from '../interfaces/omp'; export interface IMultiSigBuilder { - bid(listing: MPA, bid: MPA): Promise; - accept(listing: MPA, bid: MPA, accept: MPA): Promise; - lock(listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; - release(listing: MPA, bid: MPA, accept: MPA): Promise; - refund(listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; + bid(wallet: string, listing: MPA, bid: MPA): Promise; + accept(wallet: string, listing: MPA, bid: MPA, accept: MPA): Promise; + lock(wallet: string, listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; + release(wallet: string, listing: MPA, bid: MPA, accept: MPA): Promise; + refund(wallet: string, listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; } export interface IMadCTBuilder { - bid(listing: MPA, bid: MPA): Promise; - accept(listing: MPA, bid: MPA, accept: MPA): Promise; - lock(listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; - complete(listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; // rawtx - release(listing: MPA, bid: MPA, accept: MPA): Promise; - refund(listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; + bid(wallet: string, listing: MPA, bid: MPA): Promise; + accept(wallet: string, listing: MPA, bid: MPA, accept: MPA): Promise; + lock(wallet: string, listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; + complete(wallet: string, listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; // rawtx + release(wallet: string, listing: MPA, bid: MPA, accept: MPA): Promise; + refund(wallet: string, listing: MPA, bid: MPA, accept: MPA, lock: MPA): Promise; } diff --git a/src/buyflow/madct.ts b/src/buyflow/madct.ts index 8b54ef2b8..bc077cf8d 100644 --- a/src/buyflow/madct.ts +++ b/src/buyflow/madct.ts @@ -1,15 +1,12 @@ import { inject, injectable } from 'inversify'; import { TYPES } from '../types'; - -import { BlindPrevout, CryptoAddress, EphemeralKey, OutputType, ToBeBlindOutput } from '../interfaces/crypto'; - +import { BlindPrevout, CryptoAddress, EphemeralKey, OutputType, Prevout, ToBeBlindOutput } from '../interfaces/crypto'; import { CtRpc, ILibrary } from '../abstract/rpc'; import { IMadCTBuilder } from '../abstract/transactions'; import { Config } from '../abstract/config'; - import { buildBidTxScript, buildDestroyTxScript, ConfidentialTransactionBuilder, getExpectedSequence } from '../transaction-builder/confidential-transaction'; - import { MPA_ACCEPT, MPA_BID, MPA_LISTING_ADD, MPA_LOCK, PaymentDataAcceptCT, PaymentDataBidCT, PaymentDataLockCT } from '../interfaces/omp'; +import { EscrowReleaseType } from '../interfaces/omp-enums'; import { asyncMap, clone, isArrayAndContains, isObject } from '../util'; import { hash } from '../hasher/hash'; @@ -35,10 +32,11 @@ export class MadCTBuilder implements IMadCTBuilder { * Adds: * pubKey, changeAddress & inputs. * + * @param wallet * @param listing the marketplace listing message, used to retrieve the payment amounts. * @param bid the marketplace bid message to add the transaction details to. */ - public async bid(listing: MPA_LISTING_ADD, bid: MPA_BID): Promise { + public async bid(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID): Promise { // Get the right transaction library for the right currency. const paymentData = bid.buyer.payment as PaymentDataBidCT; const lib = this._libs(paymentData.cryptocurrency, true); @@ -49,14 +47,11 @@ export class MadCTBuilder implements IMadCTBuilder { console.log('OMP_LIB: bid() requiredSatoshis: ', requiredSatoshis); // for now, we are forcing anon - // const type = (this.network === 'testnet') ? 'anon' : 'blind'; - // paymentData.prevouts = await lib.getBlindPrevouts(type, requiredSatoshis); - // todo: why have getPrevouts and createPrevoutFrom? - paymentData.prevouts = await lib.getPrevouts(OutputType.ANON, OutputType.BLIND, requiredSatoshis); + paymentData.prevouts = await lib.getPrevouts(wallet, OutputType.ANON, OutputType.BLIND, requiredSatoshis); + console.log('OMP_LIB: bid() paymentData.prevouts: ', paymentData.prevouts); if (!paymentData.outputs) { paymentData.outputs = []; - // todo: why is an empty object being pushed to the array in here? paymentData.outputs.push({} as ToBeBlindOutput); } @@ -64,10 +59,10 @@ export class MadCTBuilder implements IMadCTBuilder { const buyer_output = paymentData.outputs[0]; buyer_output.blindFactor = lib.getRandomBlindFactor(); - buyer_output.address = await lib.getNewStealthAddressWithEphem(); + buyer_output.address = await lib.getNewStealthAddressWithEphem(wallet); + const address: CryptoAddress = await lib.getNewStealthAddressWithEphem(wallet, buyer_output.address); - const address: CryptoAddress = await lib.getNewStealthAddressWithEphem(buyer_output.address); // TODO (security): randomize value and PRESENCE. Can be undefined! -> randomizes index too const blindFactor = lib.getRandomBlindFactor(); // const blindFactor = undefined; @@ -88,13 +83,14 @@ export class MadCTBuilder implements IMadCTBuilder { * * Note: this function is also called by the buyer. * + * @param wallet * @param listing the marketplace listing message, used to retrieve the payment amounts. * @param bid the marketplace bid message to add the transaction details to. * @param accept the accept to fill in or rebuild the transaction from. */ // TODO: cognitive-complexity 28, should be less than 20 // tslint:disable:cognitive-complexity - public async accept(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { + public async accept(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { // TODO(security): safe numbers? const bidPaymentData = bid.buyer.payment as PaymentDataBidCT; @@ -106,7 +102,7 @@ export class MadCTBuilder implements IMadCTBuilder { acceptPaymentData.outputs.push({} as ToBeBlindOutput); } - if (!acceptPaymentData.outputs || acceptPaymentData.outputs.length === 0) { + if (!bidPaymentData.outputs || bidPaymentData.outputs.length === 0) { throw new Error('Missing buyer outputs.'); } @@ -146,11 +142,7 @@ export class MadCTBuilder implements IMadCTBuilder { const blind = hash(buyer_output.blindFactor + cryptocurrency.address.address); // Generate a new CT output of the _exact_ amount. - // const type = (this.network === 'testnet') ? 'anon' : 'blind'; - // for now, we are forcing anon - // acceptPaymentData.prevouts = await lib.getBlindPrevouts(type, seller_requiredSatoshis + seller_fee, blind); - // todo: why have getPrevouts and createPrevoutFrom? - acceptPaymentData.prevouts = await lib.getPrevouts(OutputType.ANON, OutputType.BLIND, seller_requiredSatoshis + seller_fee, blind); + acceptPaymentData.prevouts = await lib.getPrevouts(wallet, OutputType.ANON, OutputType.BLIND, seller_requiredSatoshis + seller_fee, blind); } @@ -165,17 +157,17 @@ export class MadCTBuilder implements IMadCTBuilder { } if (!seller_output.blindFactor) { - seller_output.blindFactor = await lib.getLastMatchingBlindFactor([seller_prevout, buyer_prevout], [buyer_output]); + seller_output.blindFactor = await lib.getLastMatchingBlindFactor(wallet, [seller_prevout, buyer_prevout], [buyer_output]); } if (!seller_output.address) { - seller_output.address = await lib.getNewStealthAddressWithEphem(); + seller_output.address = await lib.getNewStealthAddressWithEphem(wallet); } - // Load all trusted values for prevouts from blockchain. - // commitment, scriptPubKey, ... - await asyncMap(bidPaymentData.prevouts, async i => await lib.loadTrustedFieldsForBlindUtxo(i)); - await asyncMap(acceptPaymentData.prevouts, async i => await lib.loadTrustedFieldsForBlindUtxo(i)); + // Load all trusted values for prevouts from blockchain. + // commitment, scriptPubKey, ... + await asyncMap(bidPaymentData.prevouts, async i => await lib.loadTrustedFieldsForBlindUtxo(wallet, i)); + await asyncMap(acceptPaymentData.prevouts, async i => await lib.loadTrustedFieldsForBlindUtxo(wallet, i)); seller_output = this.getBidOutput(seller_output, buyer_output, 2880, seller_requiredSatoshis, true); buyer_output = this.getBidOutput(seller_output, buyer_output, 2880, buyer_requiredSatoshis); @@ -183,7 +175,7 @@ export class MadCTBuilder implements IMadCTBuilder { const all_inputs = clone(bidPaymentData.prevouts).concat(acceptPaymentData.prevouts); const all_outputs = [seller_output, buyer_output]; - const rawbidtx = await lib.generateRawConfidentialTx(all_inputs, all_outputs, seller_fee); + const rawbidtx = await lib.generateRawConfidentialTx(wallet, all_inputs, all_outputs, seller_fee); const bidtx: ConfidentialTransactionBuilder = new ConfidentialTransactionBuilder(rawbidtx); @@ -193,7 +185,7 @@ export class MadCTBuilder implements IMadCTBuilder { // import the redeem script so the wallet is aware to watch on it // losing the pubkey makes the redeem script unrecoverable. // (always import the redeem script, doesn't matter) - await lib.importRedeemScript(seller_output._redeemScript); + await lib.importRedeemScript(wallet, seller_output._redeemScript); accept['_bidtx'] = bidtx; accept['_rawbidtx'] = bidtx.build(); @@ -204,10 +196,10 @@ export class MadCTBuilder implements IMadCTBuilder { /** * Destroy transaction from bid transaction. */ - const destroy_blind_out = await lib.getLastMatchingBlindFactor([buyer_output, seller_output], []); + const destroy_blind_out = await lib.getLastMatchingBlindFactor(wallet, [buyer_output, seller_output], []); const destroy_output = this.getDestroyOutput(bidtx, seller_requiredSatoshis + buyer_requiredSatoshis - seller_fee, destroy_blind_out); - const rawdesttx = await lib.generateRawConfidentialTx(bid_utxos, destroy_output, acceptPaymentData.fee); + const rawdesttx = await lib.generateRawConfidentialTx(wallet, bid_utxos, destroy_output, acceptPaymentData.fee); const desttx: ConfidentialTransactionBuilder = new ConfidentialTransactionBuilder(rawdesttx); if (!acceptPaymentData.destroy || !isArrayAndContains(acceptPaymentData.destroy.signatures)) { @@ -215,7 +207,7 @@ export class MadCTBuilder implements IMadCTBuilder { signatures: [] }; - acceptPaymentData.destroy.signatures = await lib.signRawTransactionForBlindInputs(desttx, bid_utxos, seller_output.address); + acceptPaymentData.destroy.signatures = await lib.signRawTransactionForBlindInputs(wallet, desttx, bid_utxos, seller_output.address); } accept['_desttx'] = desttx; accept['_rawdesttx'] = desttx.build(); @@ -233,7 +225,7 @@ export class MadCTBuilder implements IMadCTBuilder { // If not rebuilding, generate new ephem key and insert in msg if (acceptPaymentData.release && !acceptPaymentData.release.ephem) { - const sx = await lib.getNewStealthAddressWithEphem(seller_output.address); + const sx = await lib.getNewStealthAddressWithEphem(wallet, seller_output.address); if (sx.ephem) { acceptPaymentData.release.ephem = sx.ephem; } @@ -249,8 +241,8 @@ export class MadCTBuilder implements IMadCTBuilder { // It is re-used for both release or refund but only one transaction is accepted, // so this shouldn't result in any trouble. // Refund is basically a release with the amount swapped around. - const buyer_release_address: CryptoAddress = await this.getReleaseAddress(lib, buyer_output.address, bidPaymentData.release.ephem); - const seller_release_address: CryptoAddress = await this.getReleaseAddress(lib, seller_output.address, acceptPaymentData.release.ephem!); + const buyer_release_address: CryptoAddress = await this.getReleaseAddress(lib, wallet, buyer_output.address, bidPaymentData.release.ephem); + const seller_release_address: CryptoAddress = await this.getReleaseAddress(lib, wallet, seller_output.address, acceptPaymentData.release.ephem!); // Decide where the sellers output is going to be located. const isSellerLastOutput = (bidPaymentData.release.blindFactor !== undefined); @@ -263,33 +255,35 @@ export class MadCTBuilder implements IMadCTBuilder { // We usually use the "derived" blind factor for the last output. (not mandatory) // If the buyer didnt provide a blind factor then it will be last. if (isSellerLastOutput) { - lastBlindFactor = await lib.getLastMatchingBlindFactor(bid_utxos, + lastBlindFactor = await lib.getLastMatchingBlindFactor(wallet, bid_utxos, [{blindFactor: bidPaymentData.release.blindFactor} as ToBeBlindOutput]); } else { // TODO(security): random acceptPaymentData.release.blindFactor = acceptPaymentData.release.blindFactor || lib.getRandomBlindFactor(); - lastBlindFactor = await lib.getLastMatchingBlindFactor( - bid_utxos, + lastBlindFactor = await lib.getLastMatchingBlindFactor(wallet, bid_utxos, [{blindFactor: acceptPaymentData.release.blindFactor} as ToBeBlindOutput]); } const buyer_blindFactor_release = isSellerLastOutput ? bidPaymentData.release.blindFactor : lastBlindFactor; const seller_blindFactor_release = isSellerLastOutput ? lastBlindFactor : acceptPaymentData.release.blindFactor!; + const releaseType = listing.item.payment.escrow && listing.item.payment.escrow.releaseType ? + listing.item.payment.escrow.releaseType : EscrowReleaseType.BLIND; + // Randomize the positioning for increased privacy. // based on whether the buyer provided a blind factor or not. const buyer_releaseSatoshis = this.release_calculateRequiredSatoshis(listing, bid, false); const seller_releaseSatoshis = this.release_calculateRequiredSatoshis(listing, bid, true); const release_outputs: ToBeBlindOutput[] = [ - this.getReleaseOutput(buyer_release_address, buyer_releaseSatoshis, buyer_blindFactor_release), // buyer_release_output - this.getReleaseOutput(seller_release_address, seller_releaseSatoshis - seller_fee, seller_blindFactor_release) // seller_release_output + this.getReleaseOutput(buyer_release_address, buyer_releaseSatoshis, buyer_blindFactor_release, releaseType), // buyer_release_output + this.getReleaseOutput(seller_release_address, seller_releaseSatoshis - seller_fee, seller_blindFactor_release, releaseType) // seller_release_output ]; const buyer_refundSatoshis = this.release_calculateRequiredSatoshis(listing, bid, false, true); const seller_refundSatoshis = this.release_calculateRequiredSatoshis(listing, bid, true, true); const refund_outputs: ToBeBlindOutput[] = [ - this.getReleaseOutput(buyer_release_address, buyer_refundSatoshis, buyer_blindFactor_release), // buyer_refund_output - this.getReleaseOutput(seller_release_address, seller_refundSatoshis - seller_fee, seller_blindFactor_release) // seller_refund_output + this.getReleaseOutput(buyer_release_address, buyer_refundSatoshis, buyer_blindFactor_release, releaseType), // buyer_refund_output + this.getReleaseOutput(seller_release_address, seller_refundSatoshis - seller_fee, seller_blindFactor_release, releaseType) // seller_refund_output ]; // If seller is not the last output, swap them. @@ -305,20 +299,20 @@ export class MadCTBuilder implements IMadCTBuilder { delete bid_utxos[1]['_sequence']; // Build the raw release transaction - const rawreleasetx = await lib.generateRawConfidentialTx(bid_utxos, release_outputs, seller_fee); + const rawreleasetx = await lib.generateRawConfidentialTx(wallet, bid_utxos, release_outputs, seller_fee); accept['_rawreleasetxunsigned'] = rawreleasetx; const releasetx: ConfidentialTransactionBuilder = new ConfidentialTransactionBuilder(rawreleasetx); // Build the raw refund transaction (unsigned!) // TODO(security): seller_fee risk to buyer? - const rawrefundtx = await lib.generateRawConfidentialTx(bid_utxos, refund_outputs, seller_fee); + const rawrefundtx = await lib.generateRawConfidentialTx(wallet, bid_utxos, refund_outputs, seller_fee); accept['_rawrefundtxunsigned'] = rawrefundtx; // Not rebuilding, seller signs release tx if (!isArrayAndContains(acceptPaymentData.release.signatures)) { // const seller_release_input = bid_utxos[0]; // [seller_release_input] - acceptPaymentData.release.signatures = await lib.signRawTransactionForBlindInputs(releasetx, bid_utxos, seller_output.address); + acceptPaymentData.release.signatures = await lib.signRawTransactionForBlindInputs(wallet, releasetx, bid_utxos, seller_output.address); } // complete the release tx but don't reveal to seller. @@ -326,7 +320,7 @@ export class MadCTBuilder implements IMadCTBuilder { const seller_release_input = bid_utxos[0]; const buyer_release_input = bid_utxos[1]; - const buyer_signatures = await lib.signRawTransactionForBlindInputs(releasetx, bid_utxos, buyer_output.address); + const buyer_signatures = await lib.signRawTransactionForBlindInputs(wallet, releasetx, bid_utxos, buyer_output.address); const seller_signatures = acceptPaymentData.release.signatures; releasetx.puzzleReleaseWitness(seller_release_input, seller_signatures[0], buyer_signatures[0]); @@ -350,12 +344,13 @@ export class MadCTBuilder implements IMadCTBuilder { * * Note: this function is also called by the seller for rebuilding. * + * @param wallet * @param listing the marketplace listing message. * @param bid the marketplace bid message. * @param accept the marketplace accept message. * @param lock the lock to fill in or rebuild the transaction from. */ - public async lock(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { + public async lock(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { // TODO(security): safe numbers? @@ -366,7 +361,7 @@ export class MadCTBuilder implements IMadCTBuilder { // Get the right transaction library for the right currency. const lib = this._libs(bidPaymentData.cryptocurrency, true); - const rebuilt = (await this.accept(listing, bid, clone(accept))); + const rebuilt = (await this.accept(wallet, listing, bid, clone(accept))); const bidtx: ConfidentialTransactionBuilder = rebuilt['_bidtx']; if (!acceptPaymentData.outputs || acceptPaymentData.outputs.length === 0) { @@ -376,9 +371,14 @@ export class MadCTBuilder implements IMadCTBuilder { if (isArrayAndContains(lockPaymentData.signatures)) { // add signatures to inputs const signature = lockPaymentData.signatures; - bidPaymentData.prevouts.forEach((out, i) => bidtx.setWitness(out, signature[i])); + + // bidPaymentData.prevouts.forEach((out, i) => bidtx.setWitness(out, signature[i])); + for (const [index, blindPrevout] of bidPaymentData.prevouts.entries()) { + bidtx.setWitness(blindPrevout, signature[index]); + } + } else { - lockPaymentData.signatures = await lib.signRawTransactionForBlindInputs(bidtx, bidPaymentData.prevouts); + lockPaymentData.signatures = await lib.signRawTransactionForBlindInputs(wallet, bidtx, bidPaymentData.prevouts); } lock['_bidtx'] = bidtx; @@ -407,7 +407,7 @@ export class MadCTBuilder implements IMadCTBuilder { lockPaymentData.destroy = { signatures: [] }; - lockPaymentData.destroy.signatures = await lib.signRawTransactionForBlindInputs(desttx, bid_utxos, buyer_output.address); + lockPaymentData.destroy.signatures = await lib.signRawTransactionForBlindInputs(wallet, desttx, bid_utxos, buyer_output.address); } if (!acceptPaymentData.destroy || acceptPaymentData.destroy.signatures.length === 0) { @@ -431,7 +431,7 @@ export class MadCTBuilder implements IMadCTBuilder { lockPaymentData.refund = { signatures: [] }; - lockPaymentData.refund.signatures = await lib.signRawTransactionForBlindInputs(refundtx, bid_utxos, buyer_output.address); + lockPaymentData.refund.signatures = await lib.signRawTransactionForBlindInputs(wallet, refundtx, bid_utxos, buyer_output.address); } lock['_refundtx'] = refundtx; @@ -447,12 +447,13 @@ export class MadCTBuilder implements IMadCTBuilder { * Generate the fully signed bid txn in response to a * lock message. (seller only) * + * @param wallet * @param listing the marketplace listing message. * @param bid the marketplace bid message. * @param accept the marketplace accept message. * @param lock the lock to fill in or rebuild the transaction from. */ - public async complete(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { + public async complete(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { const bidPaymentData = bid.buyer.payment as PaymentDataBidCT; const acceptPaymentData = accept.seller.payment as PaymentDataAcceptCT; @@ -464,34 +465,47 @@ export class MadCTBuilder implements IMadCTBuilder { // Don't trigger the signing of releasetx for buyer when rebuilding const cloned_accept = clone(accept); - const rebuiltLockMessage = (await this.lock(listing, bid, cloned_accept, clone(lock))); + const rebuiltLockMessage = (await this.lock(wallet, listing, bid, cloned_accept, clone(lock))); // console.log('OMP_LIB: rebuiltLockMessage: ', JSON.stringify(rebuiltLockMessage, null, 2)); // rebuild from accept message - const bidtx: ConfidentialTransactionBuilder = rebuiltLockMessage['_bidtx']; + const bidTxBuilder: ConfidentialTransactionBuilder = rebuiltLockMessage['_bidtx']; - const seller_inputs = clone(acceptPaymentData.prevouts); + const seller_inputs: BlindPrevout[] = clone(acceptPaymentData.prevouts); const seller_requiredSatoshis: number = this.bid_calculateRequiredSatoshis(listing, bid, true); const seller_fee = acceptPaymentData.fee; seller_inputs[0]._satoshis = seller_requiredSatoshis + seller_fee; - // console.log('OMP_LIB: seller_requiredSatoshis: ', seller_requiredSatoshis); - // console.log('OMP_LIB: seller_fee: ', seller_fee); - - await asyncMap(seller_inputs, async i => await lib.loadTrustedFieldsForBlindUtxo(i)); - - const seller_signatures = await lib.signRawTransactionForBlindInputs(bidtx, seller_inputs); - seller_inputs.forEach((out, i) => bidtx.setWitness(out, seller_signatures[i])); + console.log('OMP_LIB: seller_requiredSatoshis: ', seller_requiredSatoshis); + console.log('OMP_LIB: seller_fee: ', seller_fee); + console.log('OMP_LIB: seller_inputs: ', seller_inputs); +/* + was: + await asyncMap(seller_inputs, async utxo => await lib.loadTrustedFieldsForBlindUtxo(wallet, utxo)); + const seller_signatures = await lib.signRawTransactionForBlindInputs(wallet, bidTxBuilder, seller_inputs); + seller_inputs.forEach((out, i) => bidTxBuilder.setWitness(out, seller_signatures[i])); +*/ + for (const sellerInput of seller_inputs) { + await lib.loadTrustedFieldsForBlindUtxo(wallet, sellerInput); + const sellerSignature = await lib.signRawTransactionForBlindInput(wallet, bidTxBuilder, sellerInput); + // TODO: why is this called twice? first in signRawTransactionForBlindInput, then here again? + bidTxBuilder.setWitness(sellerInput, sellerSignature); + } - return bidtx.build(); + return bidTxBuilder.build(); } /** * Produces a fully signed release transaction when called by the buyer. * Performs two steps: both for buyer and seller. + * + * @param wallet + * @param listing the marketplace listing message. + * @param bid the marketplace bid message. + * @param accept the marketplace accept message. */ - public async release(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { + public async release(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { // TODO(security): safe numbers? const bidPaymentData = bid.buyer.payment as PaymentDataBidCT; @@ -503,12 +517,22 @@ export class MadCTBuilder implements IMadCTBuilder { const cloned_accept = clone(accept); cloned_accept['_buyerbuildrelease'] = true; // regenerate the transaction (from the messages) - const rebuilt = (await this.accept(listing, bid, cloned_accept)); + const rebuilt = (await this.accept(wallet, listing, bid, cloned_accept)); return rebuilt['_rawreleasetx']; } - public async refund(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { + /** + * Produces a fully signed release transaction when called by the buyer. + * Performs two steps: both for buyer and seller. + * + * @param wallet + * @param listing the marketplace listing message. + * @param bid the marketplace bid message. + * @param accept the marketplace accept message. + * @param lock + */ + public async refund(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { const bidPaymentData = bid.buyer.payment as PaymentDataBidCT; const acceptPaymentData = accept.seller.payment as PaymentDataAcceptCT; @@ -518,7 +542,7 @@ export class MadCTBuilder implements IMadCTBuilder { const lib = this._libs(bidPaymentData.cryptocurrency, true); // regenerate the transaction (from the messages) - const rebuilt = (await this.lock(listing, bid, clone(accept), clone(lock))); + const rebuilt = (await this.lock(wallet, listing, bid, clone(accept), clone(lock))); const bidtx: ConfidentialTransactionBuilder = rebuilt['_bidtx']; const refundtx: ConfidentialTransactionBuilder = rebuilt['_refundtx']; @@ -547,7 +571,7 @@ export class MadCTBuilder implements IMadCTBuilder { throw new Error('Missing seller destroy signatures.'); } const buyer_signatures = lockPaymentData.refund.signatures; - const seller_signatures = await lib.signRawTransactionForBlindInputs(refundtx, bid_utxos, seller_output.address); + const seller_signatures = await lib.signRawTransactionForBlindInputs(wallet, refundtx, bid_utxos, seller_output.address); refundtx.puzzleReleaseWitness(seller_release_input, seller_signatures[0], buyer_signatures[0]); refundtx.puzzleReleaseWitness(buyer_release_input, seller_signatures[1], buyer_signatures[1]); @@ -588,11 +612,11 @@ export class MadCTBuilder implements IMadCTBuilder { ]; } - private async getReleaseAddress(lib: CtRpc, sx: CryptoAddress, ephem: EphemeralKey): Promise { + private async getReleaseAddress(lib: CtRpc, wallet: string, sx: CryptoAddress, ephem: EphemeralKey): Promise { const address = clone(sx); delete address.pubKey; address.ephem = ephem; - await lib.getPubkeyForStealthWithEphem(address); + await lib.getPubkeyForStealthWithEphem(wallet, address); return address; } @@ -618,11 +642,10 @@ export class MadCTBuilder implements IMadCTBuilder { }]; } - private getReleaseOutput(address: CryptoAddress, satoshis: number, blind: string): ToBeBlindOutput { - const type = (this.network === 'testnet') ? 'anon' : 'blind'; + private getReleaseOutput(address: CryptoAddress, satoshis: number, blind: string, releaseType: EscrowReleaseType): ToBeBlindOutput { return { address, - _type: type, + _type: releaseType, _satoshis: satoshis, blindFactor: blind }; diff --git a/src/buyflow/multisig.ts b/src/buyflow/multisig.ts index 8997711cb..2b776a641 100644 --- a/src/buyflow/multisig.ts +++ b/src/buyflow/multisig.ts @@ -1,11 +1,9 @@ import { inject, injectable } from 'inversify'; import { TYPES } from '../types'; - import { CryptoAddressType } from '../interfaces/crypto'; import { ILibrary } from '../abstract/rpc'; import { IMultiSigBuilder } from '../abstract/transactions'; import { Config } from '../abstract/config'; - import { TransactionBuilder } from '../transaction-builder/transaction'; import { MPA_BID, @@ -39,25 +37,25 @@ export class MultiSigBuilder implements IMultiSigBuilder { * Adds: * pubKey, changeAddress & inputs. * - * @param config a configuration, storing the shipping details, cryptocurrency to be used etc. + * @param wallet * @param listing the marketplace mostong message, used to retrieve the payment amounts. * @param bid the marketplace bid message to add the transaction details to. */ - public async bid(listing: MPA_LISTING_ADD, bid: MPA_BID): Promise { + public async bid(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID): Promise { const bidPaymentData = bid.buyer.payment as PaymentDataBidMultisig; // Get the right transaction library for the right currency. const lib = this._libs(bidPaymentData.cryptocurrency); - bidPaymentData.pubKey = await lib.getNewPubkey(); + bidPaymentData.pubKey = await lib.getNewPubkey(wallet); bidPaymentData.changeAddress = { type: CryptoAddressType.NORMAL, - address: await lib.getNewAddress() + address: await lib.getNewAddress(wallet) }; const requiredSatoshis: number = this.bid_calculateRequiredSatoshis(listing, bid, false); - bidPaymentData.prevouts = await lib.getNormalPrevouts(requiredSatoshis); + bidPaymentData.prevouts = await lib.getNormalPrevouts(wallet, requiredSatoshis); return bid; } @@ -70,11 +68,12 @@ export class MultiSigBuilder implements IMultiSigBuilder { * Adds: * pubKey, changeAddress & inputs. * + * @param wallet * @param listing the marketplace listig action, used to retrieve the payment amounts. * @param bid the marketplace bid action that contains the lock, release and destroy signatures. * @param accept the marketplace accept action to add the transaction details to */ - public async accept(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { + public async accept(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { const bidPaymentData = bid.buyer.payment as PaymentDataBidMultisig; const acceptPaymentData = accept.seller.payment as PaymentDataAcceptMultisig; @@ -83,10 +82,10 @@ export class MultiSigBuilder implements IMultiSigBuilder { const lib = this._libs(bidPaymentData.cryptocurrency); if (!acceptPaymentData.pubKey || !acceptPaymentData.changeAddress) { - acceptPaymentData.pubKey = await lib.getNewPubkey(); + acceptPaymentData.pubKey = await lib.getNewPubkey(wallet); acceptPaymentData.changeAddress = { type: CryptoAddressType.NORMAL, - address: await lib.getNewAddress() + address: await lib.getNewAddress(wallet) }; } @@ -102,7 +101,7 @@ export class MultiSigBuilder implements IMultiSigBuilder { if (!isArrayAndContains(acceptPaymentData.prevouts)) { // add chosen prevouts to cover amount (MPA_ACCEPT) - acceptPaymentData.prevouts = await lib.getNormalPrevouts(seller_requiredSatoshis + seller_fee); + acceptPaymentData.prevouts = await lib.getNormalPrevouts(wallet, seller_requiredSatoshis + seller_fee); } // prefetch amounts for inputs @@ -135,7 +134,7 @@ export class MultiSigBuilder implements IMultiSigBuilder { // losing the pubkey makes the redeem script unrecoverable. // (always import the redeem script, doesn't matter) if (multisigOutput._redeemScript) { - await lib.importRedeemScript(multisigOutput._redeemScript); + await lib.importRedeemScript(wallet, multisigOutput._redeemScript); } @@ -145,7 +144,7 @@ export class MultiSigBuilder implements IMultiSigBuilder { acceptPaymentData.prevouts.forEach((out, i) => bidtx.addSignature(out, signature[i])); } else { - acceptPaymentData.signatures = await lib.signRawTransactionForInputs(bidtx, seller_inputs); + acceptPaymentData.signatures = await lib.signRawTransactionForInputs(wallet, bidtx, seller_inputs); } accept['_bidtx'] = bidtx; @@ -177,7 +176,7 @@ export class MultiSigBuilder implements IMultiSigBuilder { signatures: [] }; - acceptPaymentData.release.signatures = await lib.signRawTransactionForInputs(releaseTx, [multisigUtxo]); + acceptPaymentData.release.signatures = await lib.signRawTransactionForInputs(wallet, releaseTx, [multisigUtxo]); } else { releaseTx.addSignature(multisigUtxo, acceptPaymentData.release.signatures[0]); } @@ -196,12 +195,13 @@ export class MultiSigBuilder implements IMultiSigBuilder { * Adds: * pubKey, changeAddress & inputs. * + * @param wallet * @param listing the marketplace mostong message, used to retrieve the payment amounts. * @param bid the marketplace bid message to add the transaction details to. * @param accept * @param lock */ - public async lock(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { + public async lock(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { // TODO(security): safe numbers? const bidPaymentData = bid.buyer.payment as PaymentDataBidMultisig; @@ -212,14 +212,14 @@ export class MultiSigBuilder implements IMultiSigBuilder { const lib = this._libs(bidPaymentData.cryptocurrency); // rebuild from accept message - const bidtx: TransactionBuilder = (await this.accept(listing, bid, clone(accept)))['_bidtx']; + const bidtx: TransactionBuilder = (await this.accept(wallet, listing, bid, clone(accept)))['_bidtx']; if (isArrayAndContains(lockPaymentData.signatures)) { // add signatures to inputs const signature = lockPaymentData.signatures; bidPaymentData.prevouts.forEach((out, i) => bidtx.addSignature(out, signature[i])); } else { - lockPaymentData.signatures = await lib.signRawTransactionForInputs(bidtx, bidPaymentData.prevouts); + lockPaymentData.signatures = await lib.signRawTransactionForInputs(wallet, bidtx, bidPaymentData.prevouts); } lock['_bidtx'] = bidtx; @@ -252,7 +252,7 @@ export class MultiSigBuilder implements IMultiSigBuilder { signatures: [] }; - lockPaymentData.refund.signatures = await lib.signRawTransactionForInputs(refundTx, [multisigUtxo]); + lockPaymentData.refund.signatures = await lib.signRawTransactionForInputs(wallet, refundTx, [multisigUtxo]); } else { refundTx.addSignature(multisigUtxo, lockPaymentData.refund.signatures[0]); } @@ -301,11 +301,12 @@ export class MultiSigBuilder implements IMultiSigBuilder { /** * Creates a fully signed release transaction when called by the buyer. * Returns the rawtx in hex. + * @param wallet * @param listing the listing action for which a bid was created * @param bid the bid action for which we're refunding * @param accept the accept action for that bid message */ - public async release(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { + public async release(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT): Promise { const bidPaymentData = bid.buyer.payment as PaymentDataBidMultisig; @@ -313,13 +314,13 @@ export class MultiSigBuilder implements IMultiSigBuilder { const lib = this._libs(bidPaymentData.cryptocurrency); // regenerate the transaction (from the messages) - const rebuilt = (await this.accept(listing, bid, clone(accept))); + const rebuilt = (await this.accept(wallet, listing, bid, clone(accept))); const bidTx: TransactionBuilder = rebuilt['_bidtx']; const releaseTx: TransactionBuilder = rebuilt['_releasetx']; // sign for buyer const multisigUtxo = bidTx.getMultisigUtxo(bidPaymentData.pubKey, this.network); - await lib.signRawTransactionForInputs(releaseTx, [multisigUtxo]); + await lib.signRawTransactionForInputs(wallet, releaseTx, [multisigUtxo]); return releaseTx.build(); } @@ -346,12 +347,13 @@ export class MultiSigBuilder implements IMultiSigBuilder { /** * Creates a fully signed refund transaction when called by the seller. * Returns the rawtx in hex. + * @param wallet * @param listing the listing action for which a bid was created * @param bid the bid action for which we're refunding * @param accept the accept action for the bid action * @param lock */ - public async refund(listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { + public async refund(wallet: string, listing: MPA_LISTING_ADD, bid: MPA_BID, accept: MPA_ACCEPT, lock: MPA_LOCK): Promise { const bidPaymentData = bid.buyer.payment as PaymentDataBidMultisig; const acceptPaymentData = accept.seller.payment as PaymentDataAcceptMultisig; @@ -360,13 +362,13 @@ export class MultiSigBuilder implements IMultiSigBuilder { const lib = this._libs(bidPaymentData.cryptocurrency); // regenerate the transaction (from the messages) - const rebuilt = (await this.lock(listing, bid, accept, clone(lock))); + const rebuilt = (await this.lock(wallet, listing, bid, accept, clone(lock))); const bidTx: TransactionBuilder = rebuilt['_bidtx']; const refundTx: TransactionBuilder = rebuilt['_refundtx']; // sign for seller const multisigUtxo = bidTx.getMultisigUtxo(acceptPaymentData.pubKey, this.network); - await lib.signRawTransactionForInputs(refundTx, [multisigUtxo]); + await lib.signRawTransactionForInputs(wallet, refundTx, [multisigUtxo]); return refundTx.build(); } diff --git a/src/format-validators/content.ts b/src/format-validators/content.ts index c63e5297a..f23f7aaab 100644 --- a/src/format-validators/content.ts +++ b/src/format-validators/content.ts @@ -33,16 +33,16 @@ export class FV_CONTENT { throw new Error('dsn: unknown protocol'); } - if (!isString(dsn.dataId)) { + if (dsn.dataId && !isString(dsn.dataId)) { throw new Error('dsn.dataId: not a string!'); } - if (dsn.protocol === ProtocolDSN.LOCAL) { - if (!isString(dsn.encoding)) { + if (dsn.protocol === ProtocolDSN.FILE) { + if (dsn.encoding && !isString(dsn.encoding)) { throw new Error('dsn.encoding: not a string!'); } - if (!isString(dsn.data)) { + if (dsn.data && !isString(dsn.data)) { throw new Error('dsn.data: not a string!'); } } diff --git a/src/format-validators/escrow/madct.ts b/src/format-validators/escrow/madct.ts index 3bb7861ce..2e1ffdcb9 100644 --- a/src/format-validators/escrow/madct.ts +++ b/src/format-validators/escrow/madct.ts @@ -57,9 +57,9 @@ function validateBasic(payment: PaymentDataBidCT | PaymentDataAcceptCT): boolean */ function validateReleaseRefundDestroy(exit: any, expectEphem: boolean = true, expectSignatures: boolean = true): boolean { - console.log('validateReleaseRefundDestroy(), exit: ', JSON.stringify(exit, null, 2)); - console.log('validateReleaseRefundDestroy(), expectEphem: ', expectEphem); - console.log('validateReleaseRefundDestroy(), expectSignatures: ', expectSignatures); + // console.log('validateReleaseRefundDestroy(), exit: ', JSON.stringify(exit, null, 2)); + // console.log('validateReleaseRefundDestroy(), expectEphem: ', expectEphem); + // console.log('validateReleaseRefundDestroy(), expectSignatures: ', expectSignatures); if (!isObject(exit)) { throw ('missing or not an object'); diff --git a/src/format-validators/hashable.ts b/src/format-validators/hashable.ts index e890392f6..53504b434 100644 --- a/src/format-validators/hashable.ts +++ b/src/format-validators/hashable.ts @@ -1,12 +1,13 @@ // tslint:disable:max-line-length import { Validator } from 'semverv/dist/validator'; import { HashableObject } from '../hasher/hashable'; +import { HashableConfig } from '../interfaces/configs'; export class HashableValidator extends Validator { - public validate(hashable: HashableObject): boolean { + public validate(hashable: HashableObject, config: HashableConfig): boolean { // validate that the result contains all the fields defined in config - for (const configField of this.config.fields) { - if (!hashable.hasOwnProperty(configField.to)) { + for (const configField of config.fields) { + if (hashable[configField.to] === undefined) { throw new Error(configField.to + ': missing'); } } diff --git a/src/format-validators/mpa_listing_add.ts b/src/format-validators/mpa_listing_add.ts index 3a3a4a920..815defaa1 100644 --- a/src/format-validators/mpa_listing_add.ts +++ b/src/format-validators/mpa_listing_add.ts @@ -1,4 +1,4 @@ -import { Item, ItemInfo, MPA_LISTING_ADD, MPM, PaymentDataBid, PaymentInfoEscrow, PaymentOption } from '../interfaces/omp'; +import { Item, ItemInfo, MPA_LISTING_ADD, MPM, PaymentDataBid, PaymentInfoEscrow, PaymentOption, SellerData, SellerInfo } from '../interfaces/omp'; import { SaleType, MPAction, EscrowType } from '../interfaces/omp-enums'; import { isString, isObject, isArrayAndContains, isNumber, isValidPrice, isValidPercentage, isCountry, isNonNegativeNaturalNumber, isArray } from '../util'; import { FV_MPM } from './mpm'; @@ -15,6 +15,7 @@ import { FV_OBJECTS } from './objects'; export class FV_MPA_LISTING { public static validate(msg: MPM): boolean { + // validate base class FV_MPM.validate(msg); @@ -29,6 +30,22 @@ export class FV_MPA_LISTING { throw new Error('action.item: missing or not an object'); } + // Validate seller + // note, this was changed in marketplace 0.3 + if (isObject(item.seller)) { + // tslint:disable-next-line:no-collapsible-if + + const seller: SellerInfo = item.seller; + if (!isString(seller.address)) { + throw new Error('action.item.seller.address: missing or not a string'); + } + if (!isString(seller.signature)) { + throw new Error('action.item.seller.signature: missing or not a string'); + } + } else { + throw new Error('action.item.seller: missing'); + } + // TODO: to simplify this, split the validation of separate types into separate functions // TODO: create and replace Error's with more exact Exceptions, like MissingParamException('param') // Validate information @@ -68,7 +85,7 @@ export class FV_MPA_LISTING { if (isObject(information.location)) { const location = information.location; - if (location && !isCountry(location.country)) { + if (location && location.country && !isCountry(location.country)) { throw new Error('action.item.information.location.country: not a country'); } @@ -126,7 +143,6 @@ export class FV_MPA_LISTING { throw new Error('action.item.information: missing'); } - // Validate Payment if (isObject(item.payment)) { const payment = item.payment as PaymentInfoEscrow; @@ -249,9 +265,8 @@ export class FV_MPA_LISTING { throw new Error('action.item.messaging: missing elements in element=' + i); } }); - - } + // action.item.messaging is optional for now // else { // throw new Error('action.item.messaging: missing'); diff --git a/src/format-validators/mpm.ts b/src/format-validators/mpm.ts index e1f552ff9..bbaf3ba0d 100644 --- a/src/format-validators/mpm.ts +++ b/src/format-validators/mpm.ts @@ -1,5 +1,6 @@ import { MPM } from '../interfaces/omp'; import { MPAction } from '../interfaces/omp-enums'; +import { isStringAndEnumValue } from '../util'; export class FV_MPM { @@ -17,7 +18,7 @@ export class FV_MPM { throw new Error('action.type: missing'); } - if (!(msg.action.type in MPAction)) { + if (!isStringAndEnumValue(msg.action.type, MPAction)) { throw new Error('action.type: unrecognized value'); } diff --git a/src/hasher/config/listingitemadd.ts b/src/hasher/config/listingitemadd.ts index d05ce1b5d..66fb5905b 100644 --- a/src/hasher/config/listingitemadd.ts +++ b/src/hasher/config/listingitemadd.ts @@ -7,6 +7,10 @@ export class HashableListingMessageConfig extends BaseHashableConfig { from: 'generated', to: HashableCommonField.GENERATED }, { +// we can't use seller for the template right now as it would be the selected market identity and theres no relation to that right now +// from: 'item.seller.address', +// to: HashableItemField.SELLER +// }, { from: 'item.information.title', to: HashableItemField.TITLE }, { diff --git a/src/hasher/hash.ts b/src/hasher/hash.ts index c2b4934c7..f40b68924 100644 --- a/src/hasher/hash.ts +++ b/src/hasher/hash.ts @@ -18,8 +18,12 @@ export class ConfigurableHasher { */ public static hash(objectToHash: any, config: HashableConfig): string { const hashable: HashableObject = ConfigurableHasher.toHashable(objectToHash, config); - new HashableValidator(config).valid(hashable); - return this.hashInner(hashable); + // console.log('OMP_LIB: hash(), hashable: ', JSON.stringify(hashable, null, 2)); + // console.log('OMP_LIB: hash(), config: ', JSON.stringify(config, null, 2)); + new HashableValidator(config).valid(hashable, config); + const result = this.hashInner(hashable); + // console.log('OMP_LIB: ConfigurableHasher.hash(), out: ', result); + return result; } /** @@ -71,6 +75,7 @@ export function hash(v: any): string { function hashObject(unordered: object): string { const sorted = deepSortObject(unordered); + // console.log('OMP_LIB: hashObject(), sorted: ', JSON.stringify(sorted, null, 2)); const toHash = JSON.stringify(sorted); return sha256(toHash); } @@ -102,6 +107,7 @@ export function deepSortObject(unordered: any): any { // order the keys alphabetically! const result = {}; let ordered; + if (isArray(unordered)) { ordered = unordered.sort(); } else if (isObject(unordered)) { diff --git a/src/interfaces/crypto.ts b/src/interfaces/crypto.ts index 5f31c10e0..48f1f6525 100644 --- a/src/interfaces/crypto.ts +++ b/src/interfaces/crypto.ts @@ -74,9 +74,9 @@ export enum CryptoAddressType { } export enum OutputType { - PART = 'PART', - BLIND = 'BLIND', - ANON = 'ANON' + PART = 'part', + BLIND = 'blind', + ANON = 'anon' } /** diff --git a/src/interfaces/dsn.ts b/src/interfaces/dsn.ts index ff84e1571..5de0e2bfb 100644 --- a/src/interfaces/dsn.ts +++ b/src/interfaces/dsn.ts @@ -16,14 +16,16 @@ export interface DSN { export interface ContentReference { hash: string; data: DSN[]; // multiple DSN reference may point to a single piece of content. - featured: boolean; // featured image + featured: boolean; // content could be featured. + target: string; // content could have some target or relation. } /** * Protocols supported by the protocol. */ export enum ProtocolDSN { - LOCAL = 'LOCAL', + FILE = 'FILE', + REQUEST = 'REQUEST', SMSG = 'SMSG', URL = 'URL', IPFS = 'IPFS' diff --git a/src/interfaces/omp-enums.ts b/src/interfaces/omp-enums.ts index ca248d3c7..661173fa1 100644 --- a/src/interfaces/omp-enums.ts +++ b/src/interfaces/omp-enums.ts @@ -2,14 +2,14 @@ * An enum for all the marketplace actions. */ export enum MPAction { - MPA_LISTING_ADD = 'MPA_LISTING_ADD', + MPA_LISTING_ADD = 'MPA_LISTING_ADD_03', - MPA_BID = 'MPA_BID', - MPA_ACCEPT = 'MPA_ACCEPT', - MPA_REJECT = 'MPA_REJECT', - MPA_CANCEL = 'MPA_CANCEL', + MPA_BID = 'MPA_BID_03', + MPA_ACCEPT = 'MPA_ACCEPT_03', + MPA_REJECT = 'MPA_REJECT_03', + MPA_CANCEL = 'MPA_CANCEL_03', - MPA_LOCK = 'MPA_LOCK', + MPA_LOCK = 'MPA_LOCK_03', UNKNOWN = 'UNKNOWN' // used in case we receive an unknown message type and want to log it } @@ -36,6 +36,12 @@ export enum EscrowType { MAD_CT = 'MAD_CT' // Mutual assured destruction with Confidential Tx } +export enum EscrowReleaseType { + // these need to be lowercase, particld seems to be case-sensitive + BLIND = 'blind', + ANON = 'anon' +} + /** * Protocols supported by the protocol. */ @@ -48,6 +54,7 @@ export enum MessagingProtocol { */ export enum HashableItemField { TITLE = 'title', + // SELLER = 'seller', SHORT_DESC = 'shortDescription', LONG_DESC = 'longDescription', SALE_TYPE = 'saleType', diff --git a/src/interfaces/omp.ts b/src/interfaces/omp.ts index b877419c2..fa40134de 100644 --- a/src/interfaces/omp.ts +++ b/src/interfaces/omp.ts @@ -6,7 +6,7 @@ import { Prevout, CryptoAddress, Cryptocurrency, ISignature, ToBeOutput, EphemeralKey, Fiatcurrency, ToBeBlindOutput, BlindPrevout } from './crypto'; import { DSN, ContentReference } from './dsn'; -import { MPAction, SaleType, EscrowType, MessagingProtocol } from './omp-enums'; +import { MPAction, SaleType, EscrowType, EscrowReleaseType, MessagingProtocol } from './omp-enums'; import { KVS } from './common'; @@ -236,6 +236,7 @@ export interface ShippingAddress { */ export interface Item { information: ItemInfo; + seller: SellerInfo; payment: PaymentInfo; messaging: MessagingInfo; objects?: ItemObject[]; @@ -256,6 +257,11 @@ export interface ItemInfo { images?: ContentReference[]; // optional } +export interface SellerInfo { + address: string; + signature: string; +} + /** * Location for the item * @@ -331,6 +337,7 @@ export interface EscrowConfig { type: EscrowType; ratio: EscrowRatio; secondsToLock: number; + releaseType?: EscrowReleaseType; } /** diff --git a/src/interfaces/rpc.ts b/src/interfaces/rpc.ts index 95dbdbf1a..2f2b50e7a 100644 --- a/src/interfaces/rpc.ts +++ b/src/interfaces/rpc.ts @@ -66,3 +66,204 @@ export interface RpcUnspentOutput extends RpcOutput { stakeable: boolean; // (bool) Whether we have the private keys to stake this output } + + +export interface RpcBlindInput extends RpcOutput { + type: string; + scriptPubKey: string; + amount_commitment: string; + blindingfactor: string; + redeemScript?: string; + sequence?: number; +} + +export interface RpcBlindOrFeeBase { + +} + +export interface RpcBlindOutput extends RpcBlindOrFeeBase { + rangeproof_params: any; + pubkey?: string; + ephemeral_key?: string; + script?: string; + nonce?: string; + data?: any; + address?: string; + amount?: number; + data_ct_fee?: number; +} + +export interface RpcCtFeeOutput extends RpcBlindOrFeeBase { + type: string; + amount: number; + data_ct_fee: number; +} + +export interface RpcBlindSendToOutput { + address: string; + amount: number; + blindingfactor: string; +} + +export interface RpcBlockchainInfo { + chain: string; // current network name as defined in BIP70 (main, test, regtest) + blocks: number; // the current number of blocks processed in the server + headers: number; // the current number of headers we have validated + bestblockhash: string; // the hash of the currently best block + moneysupply: number; // the total amount of coin in the network + blockindexsize: number; // the total number of block headers indexed + delayedblocks: number; // the number of delayed blocks + difficulty: number; // the current difficulty + mediantime: number; // median time for the current best block + verificationprogress: number; // estimate of verification progress [0..1] + initialblockdownload: boolean; // estimate of whether this node is in Initial Block Download mode. + chainwork: string; // total amount of work in active chain, in hexadecimal + size_on_disk: number; // the estimated size of the block and undo files on disk + pruned: boolean; // if the blocks are subject to pruning + // todo: add pruning and softfork related data when needed +} + +export interface RpcWalletInfo { + walletname: string; // the wallet name + walletversion: number; // the wallet version + total_balance: number; // the total balance of the wallet in PART + balance: number; // the total confirmed balance of the wallet in PART + blind_balance: number; // the total confirmed blinded balance of the wallet in PART + anon_balance: number; // the total confirmed anon balance of the wallet in PART + staked_balance: number; // the total staked balance of the wallet in PART (non-spendable until maturity) + unconfirmed_balance: number; // the total unconfirmed balance of the wallet in PART + immature_balance: number; // the total immature balance of the wallet in PART + immature_anon_balance: number; // the total immature anon balance of the wallet in PART + reserve: number; // the reserve balance of the wallet in PART + txcount: number; // the total number of transactions in the wallet + keypoololdest: number; // the timestamp (seconds since Unix epoch) of the oldest pre-generated key in the key pool + keypoolsize: number; // how many new keys are pre-generated (only counts external keys) + keypoolsize_hd_internal: number; // how many new keys are pre-generated for internal use (used for change outputs, only appears if the wallet is + // using this feature, otherwise external keys are used) + encryptionstatus: string; // unencrypted/locked/unlocked + unlocked_until: number; // the timestamp in seconds since epoch (midnight Jan 1 1970 GMT) that the wallet is unlocked for transfers, + // or 0 if the wallet is locked + paytxfee: number; // the transaction fee configuration, set in PART/kB + hdseedid?: string; // the Hash160 of the HD account pubkey (only present when HD is enabled) + private_keys_enabled: boolean; // false if privatekeys are disabled for this wallet (enforced watch-only wallet) +} + +export interface RpcNetworkInfo { + version: number; + subversion: string; + protocolversion: number; + localservices: string; + localservices_str: string; + localrelay: boolean; + timeoffset: number; + connections: number; + dos_states: number; + networkactive: boolean; + networks: any[]; // TODO + relayfee: number; + incrementalfee: number; + localaddresses: any[]; // TODO + warnings: string; +} + +export interface RpcMnemonic { + mnemonic: string; + master: string; +} + +export interface RpcExtKeyGenesisImport { + result: string; + master_id: string; + master_label: string; + account_id: string; + account_label: string; + note: string; +} + +export interface RpcAddressBalance { + balance: string; // current balance in satoshis + received: string; // total number of satoshis received (including change) +} + +export interface RpcExtKey { + type: string; + active: string; + receive_on: string; + encrypted: string; + hardware_device: string; + label: string; + path: string; + key_type: string; + current_master: string; + root_key_id: string; + id: string; + evkey: string; + epkey: string; + num_derives: string; + num_derives_hardened: string; + created_at: string; + has_secret: string; + external_chain: string; + num_derives_external: string; + num_derives_external_h: string; + internal_chain: string; + num_derives_internal: string; + num_derives_internal_h: string; + num_derives_stealth: string; + num_derives_stealth_h: string; +} + +export interface RpcExtKeyResult { + result: string; + id: string; + key_label: string; + note: string; + account: string; + label: string; + key_info: RpcExtKeyInfo; + // "public key" // todo: fix deriveAccount response + + account_id: string; + has_secret: string; + account_label: string; + scanned_from: number; +} + +export interface RpcExtKeyInfo { + result: string; + path: string; + type: string; + version: string; + depth: string; + parent_fingerprint: string; + child_index: string; + chain_code: string; + key: string; + privkey: string; + pubkey: string; + id: string; + address: string; + checksum: string; + ext_public_key: string; + +} + +export interface RpcBalances { + mine: RpcBalanceInfo; // balances from outputs that the wallet can sign + watchonly: RpcBalanceInfo; // watchonly balances (not present if wallet does not watch anything) +} + +export interface RpcBalanceInfo { + trusted: number; // trusted balance (outputs created by the wallet or confirmed outputs) + untrusted_pending: number; // untrusted pending balance (outputs created by others that are in the mempool) + immature: number; // balance from immature coinbase outputs + used: number; // balance from coins sent to addresses that were previously spent from (*) + staked: number; // balance from staked outputs (non-spendable until maturity) + blind_trusted: number; // trusted blinded balance (outputs created by the wallet or confirmed outputs) + blind_untrusted_pending: number; // untrusted pending blinded balance (outputs created by others that are in the mempool) + blind_used: number; // balance from coins sent to addresses that were previously spent from (*) + anon_trusted: number; // trusted anon balance (outputs created by the wallet or confirmed outputs) + anon_immature: number; // immature anon balance (outputs created by the wallet or confirmed outputs below spendable depth) + anon_untrusted_pending: number; // untrusted pending anon balance (outputs created by others that are in the mempool) + // * --> potentially privacy violating, only present if avoid_reuse is set! +} diff --git a/src/omp.ts b/src/omp.ts index ce1115632..892beb8b7 100644 --- a/src/omp.ts +++ b/src/omp.ts @@ -69,14 +69,17 @@ export class OpenMarketProtocol implements OMP { } } - public async bid(config: BidConfiguration, listing: MPM): Promise { + public async bid(wallet: string, config: BidConfiguration, listing: MPM): Promise { Format.validate(listing); const actionProcessor = this.container.get(TYPES.Processor); - return await actionProcessor.bid(config, listing); + return await actionProcessor.bid(wallet, config, listing); } - public async accept(listing: MPM, bid: MPM): Promise { + // TODO: add reject + // TODO: add cancel + + public async accept(wallet: string, listing: MPM, bid: MPM): Promise { Format.validate(listing); Format.validate(bid); @@ -86,10 +89,10 @@ export class OpenMarketProtocol implements OMP { Sequence.validate([cloned_listing, cloned_bid]); const actionProcessor = this.container.get(TYPES.Processor); - return await actionProcessor.accept(cloned_listing, cloned_bid); + return await actionProcessor.accept(wallet, cloned_listing, cloned_bid); } - public async lock(listing: MPM, bid: MPM, accept: MPM): Promise { + public async lock(wallet: string, listing: MPM, bid: MPM, accept: MPM): Promise { Format.validate(listing); Format.validate(bid); Format.validate(accept); @@ -101,15 +104,15 @@ export class OpenMarketProtocol implements OMP { Sequence.validate([cloned_listing, cloned_bid, cloned_accept]); const actionProcessor = this.container.get(TYPES.Processor); - return await actionProcessor.lock(cloned_listing, cloned_bid, cloned_accept); + return await actionProcessor.lock(wallet, cloned_listing, cloned_bid, cloned_accept); } - public async complete(listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { + public async complete(wallet: string, listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { const actionProcessor = this.container.get(TYPES.Processor); - return actionProcessor.complete(listing, bid, accept, lock); + return actionProcessor.complete(wallet, listing, bid, accept, lock); } - public async release(listing: MPM, bid: MPM, accept: MPM): Promise { + public async release(wallet: string, listing: MPM, bid: MPM, accept: MPM): Promise { Format.validate(listing); Format.validate(bid); Format.validate(accept); @@ -119,10 +122,10 @@ export class OpenMarketProtocol implements OMP { Sequence.validate(chain); const actionProcessor = this.container.get(TYPES.Processor); - return await actionProcessor.release(chain[0], chain[1], chain[2]); + return await actionProcessor.release(wallet, chain[0], chain[1], chain[2]); } - public async refund(listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { + public async refund(wallet: string, listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { // TODO: validate that there are no cancels/rejects in here! Format.validate(listing); Format.validate(bid); @@ -134,7 +137,7 @@ export class OpenMarketProtocol implements OMP { Sequence.validate(chain); const actionProcessor = this.container.get(TYPES.Processor); - return await actionProcessor.refund(chain[0], chain[1], chain[2], chain[3]); + return await actionProcessor.refund(wallet, chain[0], chain[1], chain[2], chain[3]); } diff --git a/src/processor.ts b/src/processor.ts index 628b72994..4a38f4db6 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -11,7 +11,6 @@ import { import { MPAction, EscrowType } from './interfaces/omp-enums'; import { ConfigurableHasher } from './hasher/hash'; import { BidConfiguration } from './interfaces/configs'; - import { inject, injectable } from 'inversify'; import { IMadCTBuilder, IMultiSigBuilder } from './abstract/transactions'; import { TYPES } from './types'; @@ -40,10 +39,11 @@ export class Processor { * Add payment data to reply based on configured cryptocurrency & escrow. * Add shipping details to reply * + * @param wallet * @param config a bid configuration * @param listing the listing for which to produce a bid */ - public async bid(config: BidConfiguration, listing: MPM): Promise { + public async bid(wallet: string, config: BidConfiguration, listing: MPM): Promise { const mpa_listing = listing.action; config.escrow = mpa_listing.item.payment.escrow!.type; @@ -76,13 +76,13 @@ export class Processor { // add the data to the bid object. switch (config.escrow) { case EscrowType.MULTISIG: - await this._msb.bid(listing.action, bid); + await this._msb.bid(wallet, listing.action, bid); break; case EscrowType.MAD_CT: /* delete bid.buyer.payment.pubKey; delete bid.buyer.payment.changeAddress; */ - await this._madct.bid(listing.action, bid); + await this._madct.bid(wallet, listing.action, bid); break; default: throw new Error('payment.escrow type=' + config.escrow + ' does not have a valid escrow handling function for accept'); @@ -101,10 +101,11 @@ export class Processor { * Add payment data to reply based on configured cryptocurrency & escrow (bid tx). * Adds signatures for destruction transaction. * + * @param wallet * @param listing the listing. * @param bid the bid for which to produce an accept message. */ - public async accept(listing: MPM, bid: MPM): Promise { + public async accept(wallet: string, listing: MPM, bid: MPM): Promise { const mpa_bid = bid.action; const payment = mpa_bid.buyer.payment; @@ -138,14 +139,14 @@ export class Processor { switch (payment.escrow) { case EscrowType.MULTISIG: - await this._msb.accept(listing.action, bid.action, accept); + await this._msb.accept(wallet, listing.action, bid.action, accept); break; case EscrowType.MAD_CT: /* delete accept.seller.payment.pubKey; delete accept.seller.payment.changeAddress; delete accept.seller.payment.signatures; */ - await this._madct.accept(listing.action, bid.action, accept); + await this._madct.accept(wallet, listing.action, bid.action, accept); break; default: throw new Error('payment.escrow type=' + payment.escrow + ' does not have a valid escrow handling function for accept'); @@ -163,11 +164,12 @@ export class Processor { * Buyer locks a bid. * * + * @param wallet * @param listing the listing message. * @param bid the bid message. * @param accept the accept message for which to produce an lock message. */ - public async lock(listing: MPM, bid: MPM, accept: MPM): Promise { + public async lock(wallet: string, listing: MPM, bid: MPM, accept: MPM): Promise { const mpa_bid = bid.action as MPA_BID; const payment = mpa_bid.buyer.payment as PaymentDataBid; @@ -196,7 +198,7 @@ export class Processor { switch (payment.escrow) { case EscrowType.MULTISIG: - await this._msb.lock(listing.action, bid.action, accept.action, lock); + await this._msb.lock(wallet, listing.action, bid.action, accept.action, lock); break; case EscrowType.MAD_CT: /** @@ -208,7 +210,7 @@ export class Processor { (lock.buyer.payment as PaymentDataLockCT).destroy = { signatures: [] }; - await this._madct.lock(listing.action, bid.action, accept.action, lock); + await this._madct.lock(wallet, listing.action, bid.action, accept.action, lock); break; default: throw new Error('payment.escrow type=' + payment.escrow + ' does not have a valid escrow handling function for lock'); @@ -231,19 +233,20 @@ export class Processor { * Destroy txn: fully signed * Bid txn: fully signed (no real message produced). * + * @param wallet * @param listing the listing message. * @param bid the bid message. * @param accept the accept message for which to produce an lock message. * @param lock */ - public async complete(listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { + public async complete(wallet: string, listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { const mpa_bid = bid.action; const payment = mpa_bid.buyer.payment; switch (payment.escrow) { case EscrowType.MAD_CT: - return this._madct.complete(listing.action, bid.action, accept.action, lock.action); + return this._madct.complete(wallet, listing.action, bid.action, accept.action, lock.action); default: throw new Error('payment.escrow type=' + payment.escrow + ' does not have a complete stage.'); @@ -254,20 +257,21 @@ export class Processor { * Release funds * Add signatures of seller. * + * @param wallet * @param listing the listing message. * @param bid the bid message. * @param accept the accept message. */ - public async release(listing: MPM, bid: MPM, accept: MPM): Promise { + public async release(wallet: string, listing: MPM, bid: MPM, accept: MPM): Promise { const mpa_bid = bid.action; const payment = mpa_bid.buyer.payment; switch (payment.escrow) { case EscrowType.MULTISIG: - return this._msb.release(listing.action, bid.action, accept.action); + return this._msb.release(wallet, listing.action, bid.action, accept.action); case EscrowType.MAD_CT: - return this._madct.release(listing.action, bid.action, accept.action); + return this._madct.release(wallet, listing.action, bid.action, accept.action); case EscrowType.MAD: case EscrowType.FE: default: @@ -280,21 +284,22 @@ export class Processor { * Release funds * Add signatures of seller. * + * @param wallet * @param listing the listing message. * @param bid the bid message. * @param accept the accept message for which to produce an lock message. * @param lock */ - public async refund(listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { + public async refund(wallet: string, listing: MPM, bid: MPM, accept: MPM, lock: MPM): Promise { const mpa_bid = bid.action; const payment = mpa_bid.buyer.payment; switch (payment.escrow) { case EscrowType.MULTISIG: - return this._msb.refund(listing.action, bid.action, accept.action, lock.action); + return this._msb.refund(wallet, listing.action, bid.action, accept.action, lock.action); case EscrowType.MAD_CT: - return this._madct.refund(listing.action, bid.action, accept.action, lock.action); + return this._madct.refund(wallet, listing.action, bid.action, accept.action, lock.action); case EscrowType.MAD: case EscrowType.FE: default: diff --git a/src/transaction-builder/confidential-transaction.ts b/src/transaction-builder/confidential-transaction.ts index 77847f9bb..192e8eb04 100644 --- a/src/transaction-builder/confidential-transaction.ts +++ b/src/transaction-builder/confidential-transaction.ts @@ -19,9 +19,9 @@ export class ConfidentialTransactionBuilder extends TransactionBuilder { }); prevout.setWitness([ - new Buffer(sig.signature, 'hex'), - new Buffer(sig.pubKey, 'hex') - // new Buffer(input._scriptPubKey, 'hex') + Buffer.from(sig.signature, 'hex'), + Buffer.from(sig.pubKey, 'hex') + // Buffer.from(input._scriptPubKey, 'hex') ]); return true; @@ -44,8 +44,8 @@ export class ConfidentialTransactionBuilder extends TransactionBuilder { /** * Puzzle together the release witness for a party from their output. * @param bidPrevout The previous output from the bid transaction (buyer or seller) - * @param party The signatures of the buyer or seller - * @param secret The secret revealed by the buyer + * @param seller The signatures of the buyer or seller + * @param buyer The secret revealed by the buyer */ public puzzleReleaseWitness(bidPrevout: BlindPrevout, seller: ISignature, buyer: ISignature): boolean { const input = this.tx.inputs.find((tmpInput) => (tmpInput.outputIndex === bidPrevout.vout)); @@ -79,6 +79,7 @@ export class ConfidentialTransactionBuilder extends TransactionBuilder { * @param addressFrom * @param addressTo * @param secondsToLock + * @param network */ export function buildBidTxScript(addressFrom: CryptoAddress, addressTo: CryptoAddress, secondsToLock: number, network: string): any { // commitment: string, ephem: EphemeralKey, diff --git a/src/util.ts b/src/util.ts index 686521603..f08a1d5c0 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,3 +1,4 @@ +import { MPAction } from './interfaces/omp-enums'; export function isObject(v: any): boolean { return v && typeof v === 'object'; @@ -13,6 +14,11 @@ export function isArrayAndContains(v: any): boolean { // return v && Array.isArray(v); } +export function isStringAndEnumValue(value: any, stringEnum: any): boolean { + return isString(value) + && ( Object).values(stringEnum).includes(value); +} + export function isArray(v: any): boolean { return v && Array.isArray(v); } diff --git a/test/rpc-ct.stub.ts b/test/rpc-ct.stub.ts index c771295f7..ca2211441 100644 --- a/test/rpc-ct.stub.ts +++ b/test/rpc-ct.stub.ts @@ -2,10 +2,19 @@ import { injectable } from 'inversify'; import 'reflect-metadata'; import * as WebRequest from 'web-request'; import * as _ from 'lodash'; -import { CtRpc, RpcBlindSendToOutput } from '../src/abstract/rpc'; +import { CtRpc } from '../src/abstract/rpc'; import { Prevout, BlindPrevout, CryptoAddressType, CryptoAddress, OutputType } from '../src/interfaces/crypto'; import { fromSatoshis } from '../src/util'; -import { RpcAddressInfo, RpcRawTx, RpcUnspentOutput, RpcWallet, RpcWalletDir } from '../src/interfaces/rpc'; +import { + RpcAddressInfo, + RpcRawTx, + RpcUnspentOutput, + RpcWallet, + RpcWalletDir, + RpcBlindSendToOutput, + RpcMnemonic, + RpcExtKeyGenesisImport, RpcWalletInfo, RpcBlockchainInfo +} from '../src/interfaces/rpc'; @injectable() @@ -30,33 +39,36 @@ export class CtCoreRpcService extends CtRpc { this._password = password; } - public async getNewAddress(): Promise { - return await this.call('getnewaddress'); + public async getNewAddress(wallet: string): Promise { + return await this.call('getnewaddress', [], wallet); } - public async getAddressInfo(address: string): Promise { - return await this.call('getaddressinfo', [address]); + public async getAddressInfo(wallet: string, address: string): Promise { + return await this.call('getaddressinfo', [address], wallet); } - public async importAddress(address: string, label: string, rescan: boolean, p2sh: boolean): Promise { - await this.call('importaddress', [address, label, rescan, p2sh]); + public async importAddress(wallet: string, address: string, label: string, rescan: boolean, p2sh: boolean): Promise { + await this.call('importaddress', [address, label, rescan, p2sh], wallet); } - public async sendToAddress(address: string, amount: number, comment: string): Promise { - return await this.call('sendtoaddress', [address, amount, comment]); + public async sendToAddress(wallet: string, address: string, amount: number, comment: string): Promise { + return await this.call('sendtoaddress', [address, amount, comment], wallet); } - public async sendTypeTo(typeIn: OutputType, typeOut: OutputType, outputs: RpcBlindSendToOutput[]): Promise { - return await this.call('sendtypeto', [typeIn.toString().toLowerCase(), typeOut.toString().toLowerCase(), outputs]); + public async sendTypeTo(wallet: string, typeIn: OutputType, typeOut: OutputType, outputs: RpcBlindSendToOutput[]): Promise { + const txid = await this.call('sendtypeto', [typeIn.toString().toLowerCase(), typeOut.toString().toLowerCase(), outputs], wallet); + console.log('sendTypeTo, txid: ' + txid); + return txid; } // TODO: Prevout doesn't look correct, based on the help command output - public async createSignatureWithWallet(hex: string, prevtx: Prevout, address: string): Promise { - return await this.call('createsignaturewithwallet', [hex, prevtx, address]); + public async createSignatureWithWallet(wallet: string, hex: string, prevtx: Prevout, address: string): Promise { + return await this.call('createsignaturewithwallet', [hex, prevtx, address], wallet); } /** * Send a raw transaction to the network, returns txid. + * @param wallet * @param hex the raw transaction in hex format. */ public async sendRawTransaction(hex: string): Promise { @@ -66,19 +78,28 @@ export class CtCoreRpcService extends CtRpc { /** * Get a raw transaction, always in verbose mode * @param txid + * @param verbose */ - public async getRawTransaction(txid: string): Promise { - return await this.call('getrawtransaction', [txid, true]); + public async getRawTransaction(txid: string, verbose: boolean = true): Promise { + return await this.call('getrawtransaction', [txid, verbose]); } - public async listUnspent(type: OutputType, minconf: number): Promise { + /** + * Verify inputs for raw transaction (serialized, hex-encoded). + * @param params + */ + public async verifyRawTransaction(params: any[] = []): Promise { + return await this.call('verifyrawtransaction', params); + } + + public async listUnspent(wallet: string, type: OutputType, minconf: number): Promise { switch (type) { case OutputType.ANON: - return await this.call('listunspentanon', [minconf]); + return await this.call('listunspentanon', [minconf], wallet); case OutputType.BLIND: - return await this.call('listunspentblind', [minconf]); + return await this.call('listunspentblind', [minconf], wallet); case OutputType.PART: - return await this.call('listunspent', [minconf]); + return await this.call('listunspent', [minconf], wallet); default: throw Error('Invalid Output type.'); } @@ -90,18 +111,23 @@ export class CtCoreRpcService extends CtRpc { /** * Permanently locks outputs until unlocked or spent. + * @param wallet * @param unlock * @param prevouts * @param permanent */ - public async lockUnspent(unlock: boolean, prevouts: Prevout[], permanent: boolean): Promise { - return await this.call('lockunspent', [unlock, prevouts, permanent]); + public async lockUnspent(wallet: string, unlock: boolean, prevouts: Prevout[], permanent: boolean): Promise { + return await this.call('lockunspent', [unlock, prevouts, permanent], wallet); } // CtRpc required implmentations below... - public async getNewStealthAddress(): Promise { - const sx = await this.call('getnewstealthaddress'); + public async getNewStealthAddress(wallet: string, params: any[] = []): Promise { + if (wallet === undefined) { + throw new Error('Missing wallet. (getNewStealthAddress)'); + } + + const sx = await this.call('getnewstealthaddress', params, wallet); return { type: CryptoAddressType.STEALTH, address: sx @@ -112,24 +138,25 @@ export class CtCoreRpcService extends CtRpc { // return [await this.createBlindPrevoutFrom(type, satoshis, blind)]; // } - public async getPrevouts(typeIn: OutputType, typeOut: OutputType, satoshis: number, blind?: string): Promise { - return [await this.createPrevoutFrom(typeIn, typeOut, satoshis, blind)]; + public async getPrevouts(wallet: string, typeIn: OutputType, typeOut: OutputType, satoshis: number, blind?: string): Promise { + return [await this.createPrevoutFrom(wallet, typeIn, typeOut, satoshis, blind)]; } /** * Verify value commitment. * note that the amount is satoshis, which differs from the rpc api * + * @param wallet * @param commitment * @param blind * @param satoshis */ - public async verifyCommitment(commitment: string, blind: string, satoshis: number): Promise { - return (await this.call('verifycommitment', [commitment, blind, fromSatoshis(satoshis)])).result; + public async verifyCommitment(wallet: string, commitment: string, blind: string, satoshis: number): Promise { + return (await this.call('verifycommitment', [commitment, blind, fromSatoshis(satoshis)], wallet)).result; } - public async createRawTransaction(inputs: BlindPrevout[], outputs: any[]): Promise { - return await this.call('createrawtransaction', [inputs, outputs]); + public async createRawTransaction(wallet: string, inputs: BlindPrevout[], outputs: any[]): Promise { + return await this.call('createrawtransaction', [inputs, outputs], wallet); } /** @@ -189,9 +216,11 @@ export class CtCoreRpcService extends CtRpc { return await this.call('createwallet', [name, disablePrivateKeys, blank]); } - // for clarity - public async createAndLoadWallet(name: string, disablePrivateKeys: boolean = false, blank: boolean = false): Promise { - return await this.createWallet(name, disablePrivateKeys, blank); + public async setupWallet(wallet: string): Promise { + const mnemonic: RpcMnemonic = await this.mnemonic(['new']); + const extkey: RpcExtKeyGenesisImport = await this.extKeyGenesisImport(wallet, [mnemonic.mnemonic]); + const walletInfo: RpcWalletInfo = await this.getWalletInfo(wallet); + return walletInfo; } /** @@ -203,6 +232,19 @@ export class CtCoreRpcService extends CtRpc { return await this.call('loadwallet', [name]); } + public async unloadWallet(name: string): Promise { + return await this.call('unloadwallet', [name]); + } + + /** + * Returns an object containing various state info regarding blockchain processing. + * + * @returns {Promise} + */ + public async getBlockchainInfo(): Promise { + return await this.call('getblockchaininfo', []); + } + /** * Set secure messaging to use the specified wallet. * SMSG can only be enabled on one wallet. @@ -214,7 +256,53 @@ export class CtCoreRpcService extends CtRpc { return await this.call('smsgsetwallet', [name]); } - public async call(method: string, params: any[] = []): Promise { + /** + * mnemonic new|decode|addchecksum|dumpwords|listlanguages + * new ( "password" language nBytesEntropy bip44 ) + * Generate a new extended key and mnemonic + * decode "password" "mnemonic" ( bip44 ) + * Decode mnemonic + * addchecksum "mnemonic" + * Add checksum words to mnemonic + * mnemonic dumpwords ( "language" ) + * Print list of words + * mnemonic listlanguages + * Print list of supported languages + * @param params + */ + public async mnemonic(params: any[] = []): Promise { + return await this.call('mnemonic', params); + } + + /** + * extkeygenesisimport "mnemonic/key" ( "passphrase" save_bip44_root "master_label" "account_label" scan_chain_from ) + * + * Import master key from bip44 mnemonic root key and derive default account. + * Derives an extra chain from path 444444 to receive imported coin. + * + * Arguments: + * 1. mnemonic/key (string, required) The mnemonic or root extended key. + * 2. passphrase (string, optional, default=) Passphrase when importing mnemonic. + * 3. save_bip44_root (boolean, optional, default=false) Save bip44 root key to wallet. + * 4. master_label (string, optional, default=Master Key) Label for master key. + * 5. account_label (string, optional, default=Default Account) Label for account. + * 6. scan_chain_from (numeric, optional, default=0) Scan for transactions in blocks after timestamp, negative number to skip. + * + * @param wallet + * @param params + */ + public async extKeyGenesisImport(wallet: string, params: any[] = []): Promise { + return await this.call('extkeygenesisimport', params, wallet); + } + + /** + * Returns an object containing various wallet state info. + */ + public async getWalletInfo(wallet: string): Promise { + return await this.call('getwalletinfo', [], wallet); + } + + public async call(method: string, params: any[] = [], wallet?: string): Promise { const id = this.RPC_REQUEST_ID++; const postData = JSON.stringify({ jsonrpc: '2.0', @@ -223,7 +311,7 @@ export class CtCoreRpcService extends CtRpc { id }); - const url = 'http://' + this._host + ':' + this._port; + const url = this.getUrl(wallet); const options = this.getOptions(); return await WebRequest.post(url, options, postData) @@ -267,6 +355,14 @@ export class CtCoreRpcService extends CtRpc { return rpcOpts; } + private getUrl(wallet: string | undefined): string { + const url = 'http://' + this._host + ':' + this._port; + if (wallet === undefined) { + return url; + } else { + return url + '/wallet/' + wallet; + } + } } // export const node0 = new CtCoreRpcService('localhost', 19792, 'rpcuser0', 'rpcpass0'); diff --git a/test/rpc.stub.ts b/test/rpc.stub.ts index eb6d5d255..e3e2bb908 100644 --- a/test/rpc.stub.ts +++ b/test/rpc.stub.ts @@ -3,7 +3,7 @@ import 'reflect-metadata'; import * as WebRequest from 'web-request'; import * as _ from 'lodash'; import { Rpc } from '../src/abstract/rpc'; -import { RpcAddressInfo, RpcOutput, RpcRawTx, RpcUnspentOutput, RpcWallet, RpcWalletDir } from '../src/interfaces/rpc'; +import { RpcAddressInfo, RpcBlockchainInfo, RpcOutput, RpcRawTx, RpcUnspentOutput, RpcWallet, RpcWalletDir } from '../src/interfaces/rpc'; import { OutputType } from '../src/interfaces/crypto'; @injectable() @@ -28,28 +28,29 @@ export class CoreRpcService extends Rpc { this._password = password; } - public async getNewAddress(): Promise { - return await this.call('getnewaddress'); + public async getNewAddress(wallet: string): Promise { + return await this.call('getnewaddress', [], wallet); } - public async getAddressInfo(address: string): Promise { - return await this.call('getaddressinfo', [address]); + public async getAddressInfo(wallet: string, address: string): Promise { + return await this.call('getaddressinfo', [address], wallet); } - public async importAddress(address: string, label: string, rescan: boolean, p2sh: boolean): Promise { - await this.call('importaddress', [address, label, rescan, p2sh]); + public async importAddress(wallet: string, address: string, label: string, rescan: boolean, p2sh: boolean): Promise { + await this.call('importaddress', [address, label, rescan, p2sh], wallet); } - public async sendToAddress(address: string, amount: number, comment: string): Promise { - return await this.call('sendtoaddress', [address, amount, comment]); + public async sendToAddress(wallet: string, address: string, amount: number, comment: string): Promise { + return await this.call('sendtoaddress', [address, amount, comment], wallet); } - public async createSignatureWithWallet(hex: string, prevtx: RpcUnspentOutput, address: string): Promise { - return await this.call('createsignaturewithwallet', [hex, prevtx, address]); + public async createSignatureWithWallet(wallet: string, hex: string, prevtx: RpcUnspentOutput, address: string): Promise { + return await this.call('createsignaturewithwallet', [hex, prevtx, address], wallet); } /** * Send a raw transaction to the network, returns txid. + * @param wallet * @param hex the raw transaction in hex format. */ public async sendRawTransaction(hex: string): Promise { @@ -58,6 +59,7 @@ export class CoreRpcService extends Rpc { /** * Get a raw transaction, always in verbose mode + * @param wallet * @param txid * @param verbose */ @@ -65,8 +67,16 @@ export class CoreRpcService extends Rpc { return await this.call('getrawtransaction', [txid, verbose]); } - public async listUnspent(type: OutputType, minconf: number): Promise { - return await this.call('listunspent', [minconf]); + /** + * Verify inputs for raw transaction (serialized, hex-encoded). + * @param params + */ + public async verifyRawTransaction(params: any[] = []): Promise { + return await this.call('verifyrawtransaction', params); + } + + public async listUnspent(wallet: string, type: OutputType, minconf: number): Promise { + return await this.call('listunspent', [minconf], wallet); } /** @@ -75,8 +85,8 @@ export class CoreRpcService extends Rpc { * @param prevouts * @param permanent */ - public async lockUnspent(unlock: boolean = false, prevouts: RpcOutput[], permanent: boolean = true): Promise { - return await this.call('lockunspent', [unlock, prevouts, permanent]); + public async lockUnspent(wallet: string, unlock: boolean = false, prevouts: RpcOutput[], permanent: boolean = true): Promise { + return await this.call('lockunspent', [unlock, prevouts, permanent], wallet); } /** @@ -136,11 +146,6 @@ export class CoreRpcService extends Rpc { return await this.call('createwallet', [name, disablePrivateKeys, blank]); } - // for clarity - public async createAndLoadWallet(name: string, disablePrivateKeys: boolean = false, blank: boolean = false): Promise { - return await this.createWallet(name, disablePrivateKeys, blank); - } - /** * Loads a wallet from a wallet file or directory. * @@ -150,6 +155,19 @@ export class CoreRpcService extends Rpc { return await this.call('loadwallet', [name]); } + public async unloadWallet(name: string): Promise { + return await this.call('unloadwallet', [name]); + } + + /** + * Returns an object containing various state info regarding blockchain processing. + * + * @returns {Promise} + */ + public async getBlockchainInfo(): Promise { + return await this.call('getblockchaininfo', []); + } + /** * Set secure messaging to use the specified wallet. * SMSG can only be enabled on one wallet. @@ -161,7 +179,7 @@ export class CoreRpcService extends Rpc { return await this.call('smsgsetwallet', [name]); } - public async call(method: string, params: any[] = []): Promise { + public async call(method: string, params: any[] = [], wallet?: string): Promise { const id = this.RPC_REQUEST_ID++; const postData = JSON.stringify({ jsonrpc: '2.0', @@ -170,7 +188,7 @@ export class CoreRpcService extends Rpc { id }); - const url = 'http://' + this._host + ':' + this._port; + const url = this.getUrl(wallet); const options = this.getOptions(); return await WebRequest.post(url, options, postData) @@ -178,6 +196,7 @@ export class CoreRpcService extends Rpc { const jsonRpcResponse = JSON.parse(response.content); if (response.statusCode !== 200) { + const message = response.content ? JSON.parse(response.content) : response.statusMessage; if (this.DEBUG) { console.error('method:', method); @@ -212,4 +231,13 @@ export class CoreRpcService extends Rpc { return rpcOpts; } + private getUrl(wallet: string | undefined): string { + const url = 'http://' + this._host + ':' + this._port; + if (wallet === undefined) { + return url; + } else { + return url + '/wallet/' + wallet; + } + } + } diff --git a/yarn.lock b/yarn.lock index 0a4b260e8..b15a40aaf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1073,6 +1073,11 @@ define-property@^2.0.2: is-descriptor "^1.0.2" isobject "^3.0.1" +delay@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/delay/-/delay-4.3.0.tgz#efeebfb8f545579cb396b3a722443ec96d14c50e" + integrity sha512-Lwaf3zVFDMBop1yDuFZ19F9WyGcZcGacsbdlZtWjQmM50tOcMntm1njF/Nb/Vjij3KaSvCF+sEYGKrrjObu2NA== + delayed-stream@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" @@ -4077,10 +4082,10 @@ semver-diff@^2.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.0.tgz#790a7cf6fea5459bac96110b29b60412dc8ff96b" integrity sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA== -semverv@^5.6.4: - version "5.6.4" - resolved "https://registry.yarnpkg.com/semverv/-/semverv-5.6.4.tgz#bbccb3ba6ceb5f928e147d79c56ebc2219137c09" - integrity sha512-xr1KZkOi42ErYGHUpRoGbZ7ZhssUgw/6dK1msdY5g1u/r9JoRIaFwf5cuEXyRtlYorLQlpXcsnZ63zWl6/TZ1g== +semverv@^5.6.5: + version "5.6.5" + resolved "https://registry.yarnpkg.com/semverv/-/semverv-5.6.5.tgz#539404c56ab027384ecf1c2ecfad753dd5fbf4f1" + integrity sha512-oeC5Va4AlNJVfdlXmM/beEqScJF85HR3G0GgII42+kiLfYZfxYdV0RUDCdTVDk9RbiLnrlExg9KwryvtXICxkw== dependencies: js-sha256 "^0.9.0" lodash "^4.17.11"