2021-11-12 10:26:22 -05:00
// --------------------------------- IMPORTS ----------------------------------
2022-02-11 17:13:56 -06:00
2021-09-17 09:33:58 -04:00
// import haveno types
2022-04-07 16:35:48 -04:00
import { HavenoClient } from "./haveno" ;
2022-01-15 15:43:10 -05:00
import { HavenoUtils } from "./utils/HavenoUtils" ;
2021-12-08 06:22:36 -05:00
import * as grpcWeb from 'grpc-web' ;
2022-02-09 01:41:00 -08:00
import { MarketPriceInfo , NotificationMessage , OfferInfo , TradeInfo , UrlConnection , XmrBalanceInfo } from './protobuf/grpc_pb' ; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb
2022-04-04 12:29:35 -07:00
import { Attachment , DisputeResult , PaymentMethod , PaymentAccount , MoneroNodeSettings } from './protobuf/pb_pb' ;
2021-12-30 22:03:00 +02:00
import { XmrDestination , XmrTx , XmrIncomingTransfer , XmrOutgoingTransfer } from './protobuf/grpc_pb' ;
2022-02-09 01:41:00 -08:00
import AuthenticationStatus = UrlConnection . AuthenticationStatus ;
import OnlineStatus = UrlConnection . OnlineStatus ;
2021-09-12 09:39:21 -04:00
2021-09-17 09:33:58 -04:00
// import monero-javascript
const monerojs = require ( "monero-javascript" ) ; // TODO (woodser): support typescript and `npm install @types/monero-javascript` in monero-javascript
2021-10-22 13:51:57 -04:00
const GenUtils = monerojs . GenUtils ;
2022-04-06 11:28:56 -04:00
const BigInteger = monerojs . BigInteger ;
2021-12-30 22:03:00 +02:00
const MoneroNetworkType = monerojs . MoneroNetworkType ;
2021-09-19 14:00:22 -04:00
const MoneroTxConfig = monerojs . MoneroTxConfig ;
2022-04-06 11:28:56 -04:00
const MoneroDestination = monerojs . MoneroDestination ;
2021-12-30 22:03:00 +02:00
const MoneroUtils = monerojs . MoneroUtils ;
2021-09-19 14:00:22 -04:00
const TaskLooper = monerojs . TaskLooper ;
2021-12-08 06:22:36 -05:00
// other required imports
2022-02-09 01:41:00 -08:00
const fs = require ( 'fs' ) ;
2021-12-14 13:04:02 -05:00
const net = require ( 'net' ) ;
2022-02-09 01:41:00 -08:00
const assert = require ( "assert" ) ;
const console = require ( 'console' ) ; // import console because jest swallows messages in real time
2021-09-12 09:39:21 -04:00
2021-12-16 20:10:40 -05:00
// ------------------------------ TEST CONFIG ---------------------------------
const TestConfig = {
2022-02-09 01:41:00 -08:00
logLevel : 0 ,
2022-01-24 19:37:18 +01:00
moneroBinsDir : "../haveno/.localnet" ,
2022-04-04 12:29:35 -07:00
testDataDir : "./testdata" ,
2022-01-24 19:37:18 +01:00
networkType : monerojs.MoneroNetworkType.STAGENET ,
2021-12-16 20:10:40 -05:00
haveno : {
path : "../haveno" ,
version : "1.6.2"
} ,
monerod : {
2022-02-09 01:41:00 -08:00
url : "http://localhost:38081" ,
2022-01-24 19:37:18 +01:00
username : "superuser" ,
password : "abctesting123"
} ,
monerod2 : {
2022-02-09 01:41:00 -08:00
url : "http://localhost:58081" ,
2021-12-16 20:10:40 -05:00
username : "superuser" ,
password : "abctesting123"
} ,
fundingWallet : {
2022-02-09 01:41:00 -08:00
url : "http://localhost:38084" ,
2021-12-16 20:10:40 -05:00
username : "rpc_user" ,
password : "abc123" ,
defaultPath : "test_funding_wallet" ,
minimumFunding : BigInt ( "5000000000000" )
} ,
2022-02-09 01:41:00 -08:00
defaultHavenod : {
logProcessOutput : false , // log output for processes started by tests (except arbitrator, alice, and bob which are configured separately)
apiPassword : "apitest" ,
walletUsername : "haveno_user" ,
walletDefaultPassword : "password" , // only used if account password not set
accountPasswordRequired : true ,
accountPassword : "abctesting789" ,
autoLogin : true
2021-12-16 20:10:40 -05:00
} ,
2022-02-09 01:41:00 -08:00
startupHavenods : [ {
appName : "haveno-XMR_STAGENET_arbitrator" , // arbritrator
logProcessOutput : false ,
url : "http://localhost:8079" ,
accountPasswordRequired : false ,
accountPassword : "abctesting123" ,
} , {
appName : "haveno-XMR_STAGENET_alice" , // alice
logProcessOutput : false ,
url : "http://localhost:8080" ,
accountPasswordRequired : false ,
accountPassword : "abctesting456" ,
walletUrl : "http://127.0.0.1:38091" ,
} , {
appName : "haveno-XMR_STAGENET_bob" , // bob
logProcessOutput : false ,
url : "http://localhost:8081" ,
accountPasswordRequired : false ,
accountPassword : "abctesting789" ,
2022-04-06 11:28:56 -04:00
walletUrl : "http://127.0.0.1:38092" ,
2022-02-09 01:41:00 -08:00
}
] ,
2021-12-16 20:10:40 -05:00
maxFee : BigInt ( "75000000000" ) ,
2022-01-24 19:37:18 +01:00
walletSyncPeriodMs : 5000 , // TODO (woodser): auto adjust higher if using remote connection
daemonPollPeriodMs : 15000 ,
2022-04-06 11:28:56 -04:00
maxWalletStartupMs : 10000 , // TODO (woodser): make shorter by switching to jni
2021-12-16 20:10:40 -05:00
maxTimePeerNoticeMs : 3000 ,
2022-03-06 10:49:26 -05:00
assetCodes : [ "USD" , "GBP" , "EUR" , "ETH" , "BTC" , "BCH" , "LTC" , "ZEC" ] , // primary asset codes
2022-02-16 10:09:59 -05:00
cryptoAddresses : [ {
2021-12-16 20:10:40 -05:00
currencyCode : "ETH" ,
address : "0xdBdAb835Acd6fC84cF5F9aDD3c0B5a1E25fbd99f"
} , {
currencyCode : "BTC" ,
address : "bcrt1q6j90vywv8x7eyevcnn2tn2wrlg3vsjlsvt46qz"
2022-03-06 10:49:26 -05:00
} , {
currencyCode : "BCH" ,
2022-04-06 11:28:56 -04:00
address : "1JRjBNKi4ZgJpKPeoL4149Q7ZZD3VvVgk9" // TODO: support CashAddr format only
2022-02-16 11:59:31 -05:00
} , {
currencyCode : "LTC" ,
address : "LXUTUN5mTPc2LsS7cEjkyjTRcfYyJGoUuQ"
} , {
currencyCode : "ZEC" ,
address : "t1SnUTh75DSZ1AvbjiTvvHkmPoph7DeHTGG" // TODO: support z-addresses only
2021-12-16 20:10:40 -05:00
}
] ,
proxyPorts : new Map < string , string [ ] > ( [ // map proxied ports to havenod api and p2p ports
2022-02-16 10:09:59 -05:00
[ "8079" , [ "9998" , "4444" ] ] , // arbitrator
[ "8080" , [ "9999" , "5555" ] ] , // alice
[ "8081" , [ "10000" , "6666" ] ] , // bob
2021-12-16 20:10:40 -05:00
[ "8082" , [ "10001" , "7777" ] ] ,
[ "8083" , [ "10002" , "7778" ] ] ,
[ "8084" , [ "10003" , "7779" ] ] ,
[ "8085" , [ "10004" , "7780" ] ] ,
[ "8086" , [ "10005" , "7781" ] ] ,
2022-01-08 05:21:32 -08:00
] ) ,
2022-02-09 01:41:00 -08:00
devPrivilegePrivKey : "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a" , // from DEV_PRIVILEGE_PRIV_KEY
2022-04-06 11:28:56 -04:00
tradeInitTimeout : 60000 ,
2022-02-16 10:09:59 -05:00
timeout : 900000 , // timeout in ms for all tests to complete (15 minutes)
2022-04-06 11:28:56 -04:00
postOffer : { // TODO (woodser): use typed config
2022-02-16 10:09:59 -05:00
direction : "buy" , // buy or sell xmr
amount : BigInt ( "200000000000" ) , // amount of xmr to trade
assetCode : "eth" , // counter asset to trade
price : undefined , // use market price if undefined // TODO: converted to long on backend
paymentAccountId : undefined ,
2022-02-11 17:13:56 -06:00
priceMargin : 0.0 ,
minAmount : BigInt ( "150000000000" ) , // TODO: disable by default
buyerSecurityDeposit : 0.15 ,
awaitUnlockedBalance : false ,
2022-02-16 10:09:59 -05:00
triggerPrice : undefined // TODO: fails if there is a decimal, converted to long on backend
2022-02-11 17:13:56 -06:00
}
2021-12-16 20:10:40 -05:00
} ;
2021-11-12 10:26:22 -05:00
2021-12-30 22:03:00 +02:00
interface TxContext {
isCreatedTx : boolean ;
}
2021-12-16 20:10:40 -05:00
// clients
2022-04-07 16:35:48 -04:00
let startupHavenods : HavenoClient [ ] = [ ] ;
let arbitrator : HavenoClient ;
let alice : HavenoClient ;
let bob : HavenoClient ;
2021-09-17 09:33:58 -04:00
let monerod : any ;
2021-12-16 20:10:40 -05:00
let fundingWallet : any ;
let aliceWallet : any ;
2022-04-06 11:28:56 -04:00
let bobWallet : any ;
2021-12-08 06:22:36 -05:00
// track started haveno processes
2022-04-07 16:35:48 -04:00
const HAVENO_PROCESSES : HavenoClient [ ] = [ ] ;
2021-12-14 13:04:02 -05:00
const HAVENO_PROCESS_PORTS : string [ ] = [ ] ;
2021-12-08 06:22:36 -05:00
2022-02-09 01:41:00 -08:00
// other config
const OFFLINE_ERR_MSG = "Http response at 400 or 500 level" ;
2021-12-16 20:10:40 -05:00
// -------------------------- BEFORE / AFTER TESTS ----------------------------
2021-11-12 10:26:22 -05:00
2022-02-09 01:41:00 -08:00
jest . setTimeout ( TestConfig . timeout ) ;
2021-09-17 09:33:58 -04:00
beforeAll ( async ( ) = > {
2021-09-19 14:00:22 -04:00
2021-12-16 20:10:40 -05:00
// set log level for tests
2022-02-09 01:41:00 -08:00
HavenoUtils . setLogLevel ( TestConfig . logLevel ) ;
// start configured haveno daemons
let promises = [ ] ;
2022-04-05 15:18:36 -04:00
for ( let config of TestConfig . startupHavenods ) promises . push ( initHaveno ( config ) ) ;
2022-02-09 01:41:00 -08:00
for ( let settledPromise of await Promise . allSettled ( promises ) ) {
if ( settledPromise . status !== "fulfilled" ) throw new Error ( ( settledPromise as PromiseRejectedResult ) . reason ) ;
2022-04-07 16:35:48 -04:00
startupHavenods . push ( ( settledPromise as PromiseFulfilledResult < HavenoClient > ) . value ) ;
2022-02-09 01:41:00 -08:00
}
2021-12-16 20:10:40 -05:00
2022-02-09 01:41:00 -08:00
// assign arbitrator alice, bob
arbitrator = startupHavenods [ 0 ] ;
alice = startupHavenods [ 1 ] ;
bob = startupHavenods [ 2 ] ;
2021-12-16 20:10:40 -05:00
2022-01-08 05:21:32 -08:00
// register arbitrator as dispute agents
await arbitrator . registerDisputeAgent ( "mediator" , TestConfig . devPrivilegePrivKey ) ;
await arbitrator . registerDisputeAgent ( "refundagent" , TestConfig . devPrivilegePrivKey ) ;
2021-12-16 20:10:40 -05:00
// connect monero clients
2022-02-09 01:41:00 -08:00
monerod = await monerojs . connectToDaemonRpc ( TestConfig . monerod . url , TestConfig . monerod . username , TestConfig . monerod . password ) ;
aliceWallet = await monerojs . connectToWalletRpc ( TestConfig . startupHavenods [ 1 ] . walletUrl , TestConfig . defaultHavenod . walletUsername , TestConfig . startupHavenods [ 1 ] . accountPasswordRequired ? TestConfig . startupHavenods [ 1 ] . accountPassword : TestConfig.defaultHavenod.walletDefaultPassword ) ;
2022-04-06 11:28:56 -04:00
bobWallet = await monerojs . connectToWalletRpc ( TestConfig . startupHavenods [ 2 ] . walletUrl , TestConfig . defaultHavenod . walletUsername , TestConfig . startupHavenods [ 2 ] . accountPasswordRequired ? TestConfig . startupHavenods [ 2 ] . accountPassword : TestConfig.defaultHavenod.walletDefaultPassword ) ;
2021-09-19 14:00:22 -04:00
2021-11-18 15:45:25 -05:00
// initialize funding wallet
await initFundingWallet ( ) ;
2021-09-17 09:33:58 -04:00
} ) ;
2021-09-12 09:39:21 -04:00
2021-12-16 20:10:40 -05:00
beforeEach ( async ( ) = > {
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 0 , "BEFORE TEST \"" + expect . getState ( ) . currentTestName + "\"" ) ;
2021-12-16 20:10:40 -05:00
} ) ;
afterAll ( async ( ) = > {
2022-02-09 01:41:00 -08:00
let promises = [ ] ;
for ( let havenod of startupHavenods ) if ( havenod . getProcess ( ) ) promises . push ( releaseHavenoProcess ( havenod ) ) ;
return Promise . all ( promises ) ;
2021-12-16 20:10:40 -05:00
} ) ;
// ----------------------------------- TESTS ----------------------------------
2021-09-12 09:39:21 -04:00
test ( "Can get the version" , async ( ) = > {
2021-12-16 20:10:40 -05:00
let version = await arbitrator . getVersion ( ) ;
expect ( version ) . toEqual ( TestConfig . haveno . version ) ;
2021-09-12 09:39:21 -04:00
} ) ;
2022-02-09 01:41:00 -08:00
test ( "Can manage an account" , async ( ) = > {
2022-04-07 16:35:48 -04:00
let charlie : HavenoClient | undefined ;
2022-02-09 01:41:00 -08:00
let err : any ;
2022-01-08 05:21:32 -08:00
try {
2022-02-09 01:41:00 -08:00
// start charlie without opening account
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( { autoLogin : false } ) ;
2022-02-09 01:41:00 -08:00
assert ( ! await charlie . accountExists ( ) ) ;
// test errors when account not open
await testAccountNotOpen ( charlie ) ;
// create account
let password = "testPassword" ;
await charlie . createAccount ( password ) ;
2022-04-04 12:29:35 -07:00
if ( await charlie . isConnectedToMonero ( ) ) await charlie . getBalances ( ) ; // only connected if local node running
2022-02-09 01:41:00 -08:00
assert ( await charlie . accountExists ( ) ) ;
assert ( await charlie . isAccountOpen ( ) ) ;
// close account
await charlie . closeAccount ( ) ;
assert ( await charlie . accountExists ( ) ) ;
assert ( ! await charlie . isAccountOpen ( ) ) ;
await testAccountNotOpen ( charlie ) ;
// open account with wrong password
try {
await charlie . openAccount ( "wrongPassword" ) ;
throw new Error ( "Should have failed opening account with wrong password" ) ;
} catch ( err ) {
assert . equal ( err . message , "Incorrect password" ) ;
}
// open account
await charlie . openAccount ( password ) ;
assert ( await charlie . accountExists ( ) ) ;
assert ( await charlie . isAccountOpen ( ) ) ;
// restart charlie
let charlieConfig = { appName : charlie.getAppName ( ) , autoLogin : false }
await releaseHavenoProcess ( charlie ) ;
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( charlieConfig ) ;
2022-02-09 01:41:00 -08:00
assert ( await charlie . accountExists ( ) ) ;
assert ( ! await charlie . isAccountOpen ( ) ) ;
// open account
await charlie . openAccount ( password ) ;
assert ( await charlie . accountExists ( ) ) ;
assert ( await charlie . isAccountOpen ( ) ) ;
// change password
password = "newPassword" ;
await charlie . changePassword ( password ) ;
assert ( await charlie . accountExists ( ) ) ;
assert ( await charlie . isAccountOpen ( ) ) ;
// restart charlie
await releaseHavenoProcess ( charlie ) ;
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( charlieConfig ) ;
2022-02-09 01:41:00 -08:00
await testAccountNotOpen ( charlie ) ;
// open account
await charlie . openAccount ( password ) ;
assert ( await charlie . accountExists ( ) ) ;
assert ( await charlie . isAccountOpen ( ) ) ;
// backup account to zip file
2022-04-04 12:29:35 -07:00
let zipFile = TestConfig . testDataDir + "/backup.zip" ;
2022-02-09 01:41:00 -08:00
let stream = fs . createWriteStream ( zipFile ) ;
let size = await charlie . backupAccount ( stream ) ;
stream . end ( ) ;
assert ( size > 0 ) ;
// delete account which shuts down server
await charlie . deleteAccount ( ) ;
assert ( ! await charlie . isConnectedToDaemon ( ) ) ;
await releaseHavenoProcess ( charlie ) ;
// restore account which shuts down server
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( charlieConfig ) ;
2022-02-09 01:41:00 -08:00
let zipBytes : Uint8Array = new Uint8Array ( fs . readFileSync ( zipFile ) ) ;
await charlie . restoreAccount ( zipBytes ) ;
assert ( ! await charlie . isConnectedToDaemon ( ) ) ;
await releaseHavenoProcess ( charlie ) ;
// open restored account
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( charlieConfig ) ;
2022-02-09 01:41:00 -08:00
assert ( await charlie . accountExists ( ) ) ;
await charlie . openAccount ( password ) ;
assert ( await charlie . isAccountOpen ( ) ) ;
} catch ( err2 ) {
console . log ( err2 ) ;
err = err2 ;
2022-01-09 17:02:43 +01:00
}
2022-01-24 19:37:18 +01:00
2022-02-09 01:41:00 -08:00
// stop processes
if ( charlie ) await releaseHavenoProcess ( charlie ) ;
// TODO: how to delete trader app folder at end of test?
if ( err ) throw err ;
2022-04-07 16:35:48 -04:00
async function testAccountNotOpen ( havenod : HavenoClient ) : Promise < void > { // TODO: generalize this?
2022-02-09 01:41:00 -08:00
try { await havenod . getMoneroConnections ( ) ; throw new Error ( "Should have thrown" ) ; }
catch ( err ) { assert . equal ( err . message , "Account not open" ) ; }
try { await havenod . getXmrTxs ( ) ; throw new Error ( "Should have thrown" ) ; }
catch ( err ) { assert . equal ( err . message , "Account not open" ) ; }
try { await havenod . getBalances ( ) ; throw new Error ( "Should have thrown" ) ; }
catch ( err ) { assert . equal ( err . message , "Account not open" ) ; }
2022-01-09 17:02:43 +01:00
}
} ) ;
2022-01-24 19:37:18 +01:00
test ( "Can manage Monero daemon connections" , async ( ) = > {
let monerod2 : any ;
2022-04-07 16:35:48 -04:00
let charlie : HavenoClient | undefined ;
2022-01-24 19:37:18 +01:00
let err : any ;
try {
// start charlie
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( ) ;
2022-01-24 19:37:18 +01:00
// test default connections
2022-04-04 12:29:35 -07:00
let monerodUrl1 = "http://127.0.0.1:38081" ; // TODO: (woodser): move to config
2022-02-09 01:41:00 -08:00
let monerodUrl2 = "http://haveno.exchange:38081" ;
let connections : UrlConnection [ ] = await charlie . getMoneroConnections ( ) ;
testConnection ( getConnection ( connections , monerodUrl1 ) ! , monerodUrl1 , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
testConnection ( getConnection ( connections , monerodUrl2 ) ! , monerodUrl2 , OnlineStatus . UNKNOWN , AuthenticationStatus . NO_AUTHENTICATION , 2 ) ;
2022-01-24 19:37:18 +01:00
// test default connection
2022-02-09 01:41:00 -08:00
let connection : UrlConnection | undefined = await charlie . getMoneroConnection ( ) ;
assert ( await charlie . isConnectedToMonero ( ) ) ;
testConnection ( connection ! , monerodUrl1 , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
2022-01-24 19:37:18 +01:00
// add a new connection
2022-02-09 01:41:00 -08:00
let fooBarUrl = "http://foo.bar" ;
await charlie . addMoneroConnection ( fooBarUrl ) ;
2022-01-24 19:37:18 +01:00
connections = await charlie . getMoneroConnections ( ) ;
2022-02-09 01:41:00 -08:00
connection = getConnection ( connections , fooBarUrl ) ;
testConnection ( connection ! , fooBarUrl , OnlineStatus . UNKNOWN , AuthenticationStatus . NO_AUTHENTICATION , 0 ) ;
2022-01-24 19:37:18 +01:00
// set prioritized connection without credentials
2022-02-09 01:41:00 -08:00
await charlie . setMoneroConnection ( new UrlConnection ( )
. setUrl ( TestConfig . monerod2 . url )
2022-01-24 19:37:18 +01:00
. setPriority ( 1 ) ) ;
connection = await charlie . getMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( connection ! , TestConfig . monerod2 . url , undefined , undefined , 1 ) ; // status may or may not be known due to periodic connection checking
2022-01-24 19:37:18 +01:00
// connection is offline
connection = await charlie . checkMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
assert ( ! await charlie . isConnectedToMonero ( ) ) ;
testConnection ( connection ! , TestConfig . monerod2 . url , OnlineStatus . OFFLINE , AuthenticationStatus . NO_AUTHENTICATION , 1 ) ;
2022-01-24 19:37:18 +01:00
// start monerod2
let cmd = [
TestConfig . moneroBinsDir + "/monerod" ,
"--" + monerojs . MoneroNetworkType . toString ( TestConfig . networkType ) . toLowerCase ( ) ,
"--no-igd" ,
"--hide-my-port" ,
2022-04-04 12:29:35 -07:00
"--data-dir" , TestConfig . moneroBinsDir + "/stagenet/testnode" ,
2022-01-24 19:37:18 +01:00
"--p2p-bind-port" , "58080" ,
"--rpc-bind-port" , "58081" ,
"--rpc-login" , "superuser:abctesting123" ,
"--zmq-rpc-bind-port" , "58082"
] ;
monerod2 = await monerojs . connectToDaemonRpc ( cmd ) ;
// connection is online and not authenticated
connection = await charlie . checkMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
assert ( ! await charlie . isConnectedToMonero ( ) ) ;
testConnection ( connection ! , TestConfig . monerod2 . url , OnlineStatus . ONLINE , AuthenticationStatus . NOT_AUTHENTICATED , 1 ) ;
2022-01-24 19:37:18 +01:00
// set connection credentials
2022-02-09 01:41:00 -08:00
await charlie . setMoneroConnection ( new UrlConnection ( )
. setUrl ( TestConfig . monerod2 . url )
2022-01-24 19:37:18 +01:00
. setUsername ( TestConfig . monerod2 . username )
. setPassword ( TestConfig . monerod2 . password )
. setPriority ( 1 ) ) ;
connection = await charlie . getMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( connection ! , TestConfig . monerod2 . url , undefined , undefined , 1 ) ;
2022-01-24 19:37:18 +01:00
// connection is online and authenticated
connection = await charlie . checkMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
assert ( await charlie . isConnectedToMonero ( ) ) ;
testConnection ( connection ! , TestConfig . monerod2 . url , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
// change account password
let password = "newPassword" ;
await charlie . changePassword ( "newPassword" ) ;
2022-01-24 19:37:18 +01:00
// restart charlie
let appName = charlie . getAppName ( ) ;
2022-02-09 01:41:00 -08:00
await releaseHavenoProcess ( charlie ) ;
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( { appName : appName , accountPassword : password } ) ;
2022-01-24 19:37:18 +01:00
// connection is restored, online, and authenticated
connection = await charlie . getMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( connection ! , TestConfig . monerod2 . url , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
2022-01-24 19:37:18 +01:00
connections = await charlie . getMoneroConnections ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( getConnection ( connections , monerodUrl1 ) ! , monerodUrl1 , OnlineStatus . UNKNOWN , AuthenticationStatus . NO_AUTHENTICATION , 1 ) ;
2022-01-24 19:37:18 +01:00
// enable auto switch
await charlie . setAutoSwitch ( true ) ;
// stop monerod
await monerod2 . stopProcess ( ) ;
// test auto switch after periodic connection check
await wait ( TestConfig . daemonPollPeriodMs ) ;
connection = await charlie . getMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( connection ! , monerodUrl1 , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
2022-01-24 19:37:18 +01:00
// stop checking connection periodically
await charlie . stopCheckingConnection ( ) ;
// remove current connection
2022-02-09 01:41:00 -08:00
await charlie . removeMoneroConnection ( monerodUrl1 ) ;
2022-01-24 19:37:18 +01:00
// check current connection
connection = await charlie . checkMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
assert . equal ( connection , undefined ) ;
2022-01-24 19:37:18 +01:00
// check all connections
await charlie . checkMoneroConnections ( ) ;
connections = await charlie . getMoneroConnections ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( getConnection ( connections , fooBarUrl ) ! , fooBarUrl , OnlineStatus . OFFLINE , AuthenticationStatus . NO_AUTHENTICATION , 0 ) ;
for ( let connection of connections ) testConnection ( connection ! , connection . getUrl ( ) , OnlineStatus . OFFLINE , AuthenticationStatus . NO_AUTHENTICATION ) ;
2022-01-24 19:37:18 +01:00
2022-02-09 01:41:00 -08:00
// set connection to previous url
await charlie . setMoneroConnection ( fooBarUrl ) ;
2022-01-24 19:37:18 +01:00
connection = await charlie . getMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( connection ! , fooBarUrl , OnlineStatus . OFFLINE , AuthenticationStatus . NO_AUTHENTICATION , 0 ) ;
2022-01-24 19:37:18 +01:00
2022-02-09 01:41:00 -08:00
// set connection to new url
let fooBarUrl2 = "http://foo.bar.xyz" ;
await charlie . setMoneroConnection ( fooBarUrl2 ) ;
2022-01-24 19:37:18 +01:00
connections = await charlie . getMoneroConnections ( ) ;
2022-02-09 01:41:00 -08:00
connection = getConnection ( connections , fooBarUrl2 ) ;
testConnection ( connection ! , fooBarUrl2 , OnlineStatus . UNKNOWN , AuthenticationStatus . NO_AUTHENTICATION , 0 ) ;
2022-01-24 19:37:18 +01:00
// reset connection
await charlie . setMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
assert . equal ( await charlie . getMoneroConnection ( ) , undefined ) ;
2022-01-24 19:37:18 +01:00
// test auto switch after start checking connection
await charlie . setAutoSwitch ( false ) ;
await charlie . startCheckingConnection ( 5000 ) ; // checks the connection
await charlie . setAutoSwitch ( true ) ;
2022-02-09 01:41:00 -08:00
await charlie . addMoneroConnection ( new UrlConnection ( )
. setUrl ( TestConfig . monerod . url )
2022-01-24 19:37:18 +01:00
. setUsername ( TestConfig . monerod . username )
. setPassword ( TestConfig . monerod . password )
. setPriority ( 2 ) ) ;
await wait ( 10000 ) ;
connection = await charlie . getMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( connection ! , TestConfig . monerod . url , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 2 ) ;
2022-01-24 19:37:18 +01:00
} catch ( err2 ) {
err = err2 ;
2021-10-22 13:51:57 -04:00
}
2021-11-19 23:29:44 +01:00
2022-01-24 19:37:18 +01:00
// stop processes
2022-02-09 01:41:00 -08:00
if ( charlie ) await releaseHavenoProcess ( charlie ) ;
2022-01-24 19:37:18 +01:00
if ( monerod2 ) await monerod2 . stopProcess ( ) ;
// TODO: how to delete trader app folder at end of test?
if ( err ) throw err ;
2021-10-22 13:51:57 -04:00
} ) ;
2022-04-04 12:29:35 -07:00
test ( "Can start and stop a local Monero node" , async ( ) = > {
// expect error stopping local node
try {
await alice . stopMoneroNode ( ) ;
HavenoUtils . log ( 1 , "Running local Monero node stopped" ) ;
await alice . stopMoneroNode ( ) ; // stop 2nd time to force error
throw new Error ( "should have thrown" ) ;
} catch ( err ) {
if ( err . message !== "Local Monero node is not running" &&
err . message !== "Cannot stop local Monero node because we don't own its process" ) {
throw new Error ( "Unexpected error: " + err . message ) ;
}
}
let isMoneroNodeRunning = await alice . isMoneroNodeRunning ( ) ;
if ( isMoneroNodeRunning ) {
HavenoUtils . log ( 0 , "Warning: local Monero node is already running, skipping start and stop local Monero node tests" ) ;
// expect error due to existing running node
let newSettings = new MoneroNodeSettings ( ) ;
try {
await alice . startMoneroNode ( newSettings ) ;
throw new Error ( "should have thrown" ) ;
} catch ( err ) {
if ( err . message !== "Local Monero node already running" ) throw new Error ( "Unexpected error: " + err . message ) ;
}
} else {
// expect error when passing in bad arguments
let badSettings = new MoneroNodeSettings ( ) ;
badSettings . setStartupFlagsList ( [ "--invalid-flag" ] ) ;
try {
await alice . startMoneroNode ( badSettings ) ;
throw new Error ( "should have thrown" ) ;
} catch ( err ) {
if ( ! err . message . startsWith ( "Failed to start monerod:" ) ) throw new Error ( "Unexpected error: " ) ;
}
// expect successful start with custom settings
let connectionsBefore = await alice . getMoneroConnections ( ) ;
let settings : MoneroNodeSettings = new MoneroNodeSettings ( ) ;
let dataDir = TestConfig . moneroBinsDir + "/stagenet/node1" ;
let logFile = dataDir + "/test.log" ;
let p2pPort = 38080 ;
let rpcPort = 38081 ;
settings . setBlockchainPath ( dataDir ) ;
settings . setStartupFlagsList ( [ "--log-file" , logFile , "--p2p-bind-port" , p2pPort . toString ( ) , "--rpc-bind-port" , rpcPort . toString ( ) , "--no-zmq" ] ) ;
await alice . startMoneroNode ( settings ) ;
isMoneroNodeRunning = await alice . isMoneroNodeRunning ( ) ;
assert ( isMoneroNodeRunning ) ;
// expect settings are updated
let settingsAfter = await alice . getMoneroNodeSettings ( ) ;
testMoneroNodeSettings ( settings , settingsAfter ! ) ;
// expect connections to be unmodified by local node
let connectionsAfter = await alice . getMoneroConnections ( ) ;
assert ( connectionsBefore . length === connectionsAfter . length ) ;
// expect connection to local monero node to succeed
let rpcUrl = "http://127.0.0.1:" + rpcPort . toString ( ) ;
let daemon = await monerojs . connectToDaemonRpc ( rpcUrl , "superuser" , "abctesting123" ) ;
let height = await daemon . getHeight ( ) ;
assert ( height >= 0 ) ;
// expect error due to existing running node
let newSettings = new MoneroNodeSettings ( ) ;
try {
await alice . startMoneroNode ( newSettings ) ;
throw new Error ( "should have thrown" ) ;
} catch ( err ) {
if ( err . message !== "Local Monero node already running" ) throw new Error ( "Unexpected error: " + err . message ) ;
}
// expect stopped node
await alice . stopMoneroNode ( ) ;
isMoneroNodeRunning = await alice . isMoneroNodeRunning ( ) ;
assert ( ! isMoneroNodeRunning ) ;
try {
daemon = await monerojs . connectToDaemonRpc ( rpcUrl ) ;
height = await daemon . getHeight ( ) ;
throw new Error ( "should have thrown" ) ;
} catch ( err ) {
if ( err . message !== "RequestError: Error: connect ECONNREFUSED 127.0.0.1:" + rpcPort . toString ( ) ) throw new Error ( "Unexpected error: " + err . message ) ;
}
}
} ) ;
2021-12-30 22:03:00 +02:00
// test wallet balances, transactions, deposit addresses, create and relay txs
test ( "Has a Monero wallet" , async ( ) = > {
// wait for alice to have unlocked balance
let tradeAmount : bigint = BigInt ( "250000000000" ) ;
await waitForUnlockedBalance ( tradeAmount * BigInt ( "2" ) , alice ) ;
// test balances
let balancesBefore : XmrBalanceInfo = await alice . getBalances ( ) ; // TODO: rename to getXmrBalances() for consistency?
expect ( BigInt ( balancesBefore . getUnlockedBalance ( ) ) ) . toBeGreaterThan ( BigInt ( "0" ) ) ;
expect ( BigInt ( balancesBefore . getBalance ( ) ) ) . toBeGreaterThanOrEqual ( BigInt ( balancesBefore . getUnlockedBalance ( ) ) ) ;
// get transactions
let txs : XmrTx [ ] = await alice . getXmrTxs ( ) ;
assert ( txs . length > 0 ) ;
for ( let tx of txs ) {
testTx ( tx , { isCreatedTx : false } ) ;
}
// get new deposit addresses
for ( let i = 0 ; i < 0 ; i ++ ) {
2022-04-05 14:34:19 -04:00
let address = await alice . getNewDepositAddress ( ) ;
2021-12-30 22:03:00 +02:00
MoneroUtils . validateAddress ( address , MoneroNetworkType . STAGNET ) ;
}
// create withdraw tx
2022-04-05 14:34:19 -04:00
let destination = new XmrDestination ( ) . setAddress ( await alice . getNewDepositAddress ( ) ) . setAmount ( "100000000000" ) ;
2021-12-30 22:03:00 +02:00
let tx = await alice . createXmrTx ( [ destination ] ) ;
testTx ( tx , { isCreatedTx : true } ) ;
// relay withdraw tx
let txHash = await alice . relayXmrTx ( tx . getMetadata ( ) ) ;
expect ( txHash . length ) . toEqual ( 64 ) ;
// balances decreased
let balancesAfter = await alice . getBalances ( ) ;
expect ( BigInt ( balancesAfter . getBalance ( ) ) ) . toBeLessThan ( BigInt ( balancesBefore . getBalance ( ) ) ) ;
expect ( BigInt ( balancesAfter . getUnlockedBalance ( ) ) ) . toBeLessThan ( BigInt ( balancesBefore . getUnlockedBalance ( ) ) ) ;
// get relayed tx
tx = await alice . getXmrTx ( txHash ) ;
testTx ( tx , { isCreatedTx : false } ) ;
// relay invalid tx
try {
await alice . relayXmrTx ( "invalid tx metadata" ) ;
throw new Error ( "Cannot relay invalid tx metadata" ) ;
} catch ( err ) {
if ( err . message !== "Failed to parse hex." ) throw new Error ( "Unexpected error: " + err . message ) ;
}
} ) ;
2021-09-17 09:33:58 -04:00
test ( "Can get balances" , async ( ) = > {
let balances : XmrBalanceInfo = await alice . getBalances ( ) ;
2021-11-11 13:48:31 -05:00
expect ( BigInt ( balances . getUnlockedBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
expect ( BigInt ( balances . getLockedBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
expect ( BigInt ( balances . getReservedOfferBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
expect ( BigInt ( balances . getReservedTradeBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
2021-09-12 09:39:21 -04:00
} ) ;
2022-02-09 01:41:00 -08:00
test ( "Can receive push notifications" , async ( ) = > {
// add notification listener
let notifications : NotificationMessage [ ] = [ ] ;
await alice . addNotificationListener ( notification = > {
notifications . push ( notification ) ;
} ) ;
// send test notification
for ( let i = 0 ; i < 3 ; i ++ ) {
await alice . _sendNotification ( new NotificationMessage ( )
. setTimestamp ( Date . now ( ) )
. setTitle ( "Test title " + i )
. setMessage ( "Test message " + i ) ) ;
}
// test notification
await wait ( 1000 ) ;
assert ( notifications . length >= 3 ) ;
for ( let i = 0 ; i < 3 ; i ++ ) {
assert ( notifications [ i ] . getTimestamp ( ) > 0 ) ;
assert . equal ( notifications [ i ] . getTitle ( ) , "Test title " + i ) ;
assert . equal ( notifications [ i ] . getMessage ( ) , "Test message " + i ) ;
}
} ) ;
2022-01-24 19:37:18 +01:00
test ( "Can get market prices" , async ( ) = > {
// get all market prices
let prices : MarketPriceInfo [ ] = await alice . getPrices ( ) ;
expect ( prices . length ) . toBeGreaterThan ( 1 ) ;
for ( let price of prices ) {
expect ( price . getCurrencyCode ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( price . getPrice ( ) ) . toBeGreaterThanOrEqual ( 0 ) ;
}
2022-02-16 10:09:59 -05:00
// get market prices of primary assets
for ( let assetCode of TestConfig . assetCodes ) {
let price = await alice . getPrice ( assetCode ) ;
2022-01-24 19:37:18 +01:00
expect ( price ) . toBeGreaterThan ( 0 ) ;
}
// test that prices are reasonable
let usd = await alice . getPrice ( "USD" ) ;
expect ( usd ) . toBeGreaterThan ( 50 ) ;
expect ( usd ) . toBeLessThan ( 5000 ) ;
let doge = await alice . getPrice ( "DOGE" ) ;
expect ( doge ) . toBeGreaterThan ( 200 )
expect ( doge ) . toBeLessThan ( 20000 ) ;
let btc = await alice . getPrice ( "BTC" ) ;
expect ( btc ) . toBeGreaterThan ( 0.0004 )
expect ( btc ) . toBeLessThan ( 0.4 ) ;
// test invalid currency
2022-04-11 17:52:21 -04:00
await expect ( async ( ) = > { await alice . getPrice ( "INVALID_CURRENCY" ) } )
2022-01-24 19:37:18 +01:00
. rejects
. toThrow ( 'Currency not found: INVALID_CURRENCY' ) ;
} ) ;
2022-02-11 17:13:56 -06:00
test ( "Can get market depth" , async ( ) = > {
2022-02-16 10:09:59 -05:00
let assetCode = "eth" ;
2022-02-11 17:13:56 -06:00
// clear offers
2022-02-16 10:09:59 -05:00
await clearOffers ( alice , assetCode ) ;
await clearOffers ( bob , assetCode ) ;
2022-04-07 16:35:48 -04:00
async function clearOffers ( havenod : HavenoClient , assetCode : string ) {
2022-02-16 10:09:59 -05:00
for ( let offer of await havenod . getMyOffers ( assetCode ) ) {
if ( offer . getBaseCurrencyCode ( ) . toLowerCase ( ) === assetCode . toLowerCase ( ) ) { // TODO (woodser): offer base currency and counter currency are switched for cryptos
2022-02-11 17:13:56 -06:00
await havenod . removeOffer ( offer . getId ( ) ) ;
}
}
}
// market depth has no data
await wait ( TestConfig . maxTimePeerNoticeMs ) ;
2022-02-16 10:09:59 -05:00
let marketDepth = await alice . getMarketDepth ( assetCode ) ;
2022-02-11 17:13:56 -06:00
expect ( marketDepth . getBuyPricesList ( ) . length ) . toEqual ( 0 ) ;
expect ( marketDepth . getBuyDepthList ( ) . length ) . toEqual ( 0 ) ;
expect ( marketDepth . getSellPricesList ( ) . length ) . toEqual ( 0 ) ;
expect ( marketDepth . getSellDepthList ( ) . length ) . toEqual ( 0 ) ;
// post offers to buy and sell
2022-02-16 10:09:59 -05:00
await postOffer ( alice , { direction : "buy" , amount : BigInt ( "150000000000" ) , assetCode : assetCode , priceMargin : 0.00 , awaitUnlockedBalance : true , price : 17.0 } ) ; // TODO: offer price is reversed. fix everywhere
await postOffer ( alice , { direction : "buy" , amount : BigInt ( "150000000000" ) , assetCode : assetCode , priceMargin : 0.02 , awaitUnlockedBalance : true , price : 17.2 } ) ;
await postOffer ( alice , { direction : "buy" , amount : BigInt ( "200000000000" ) , assetCode : assetCode , priceMargin : 0.05 , awaitUnlockedBalance : true , price : 17.3 } ) ;
await postOffer ( alice , { direction : "buy" , amount : BigInt ( "150000000000" ) , assetCode : assetCode , priceMargin : 0.02 , awaitUnlockedBalance : true , price : 17.3 } ) ;
await postOffer ( alice , { direction : "sell" , amount : BigInt ( "300000000000" ) , assetCode : assetCode , priceMargin : 0.00 , awaitUnlockedBalance : true } ) ;
await postOffer ( alice , { direction : "sell" , amount : BigInt ( "300000000000" ) , assetCode : assetCode , priceMargin : 0.02 , awaitUnlockedBalance : true } ) ;
await postOffer ( alice , { direction : "sell" , amount : BigInt ( "400000000000" ) , assetCode : assetCode , priceMargin : 0.05 , awaitUnlockedBalance : true } ) ;
2022-02-11 17:13:56 -06:00
// get bob's market depth
await wait ( TestConfig . maxTimePeerNoticeMs ) ;
2022-02-16 10:09:59 -05:00
marketDepth = await alice . getMarketDepth ( assetCode ) ;
2022-02-11 17:13:56 -06:00
// each unique price has a depth
expect ( marketDepth . getBuyPricesList ( ) . length ) . toEqual ( 3 ) ;
expect ( marketDepth . getSellPricesList ( ) . length ) . toEqual ( 3 ) ;
expect ( marketDepth . getBuyPricesList ( ) . length ) . toEqual ( marketDepth . getBuyDepthList ( ) . length ) ;
expect ( marketDepth . getSellPricesList ( ) . length ) . toEqual ( marketDepth . getSellDepthList ( ) . length ) ;
// test buy prices and depths
const priceDivisor = 100000000 ; // TODO: offer price = price * 100000000
2022-02-16 10:09:59 -05:00
let buyOffers = ( await alice . getOffers ( assetCode , "buy" ) ) . concat ( await alice . getMyOffers ( assetCode , "buy" ) ) . sort ( function ( a , b ) { return a . getPrice ( ) - b . getPrice ( ) } ) ;
2022-02-11 17:13:56 -06:00
expect ( marketDepth . getBuyPricesList ( ) [ 0 ] ) . toEqual ( 1 / ( buyOffers [ 0 ] . getPrice ( ) / priceDivisor ) ) ; // TODO: price when posting offer is reversed. this assumes crypto counter currency
expect ( marketDepth . getBuyPricesList ( ) [ 1 ] ) . toEqual ( 1 / ( buyOffers [ 1 ] . getPrice ( ) / priceDivisor ) ) ;
expect ( marketDepth . getBuyPricesList ( ) [ 2 ] ) . toEqual ( 1 / ( buyOffers [ 2 ] . getPrice ( ) / priceDivisor ) ) ;
expect ( marketDepth . getBuyDepthList ( ) [ 0 ] ) . toEqual ( 0.15 ) ;
expect ( marketDepth . getBuyDepthList ( ) [ 1 ] ) . toEqual ( 0.30 ) ;
expect ( marketDepth . getBuyDepthList ( ) [ 2 ] ) . toEqual ( 0.65 ) ;
// test sell prices and depths
2022-02-16 10:09:59 -05:00
let sellOffers = ( await alice . getOffers ( assetCode , "sell" ) ) . concat ( await alice . getMyOffers ( assetCode , "sell" ) ) . sort ( function ( a , b ) { return b . getPrice ( ) - a . getPrice ( ) } ) ;
2022-02-11 17:13:56 -06:00
expect ( marketDepth . getSellPricesList ( ) [ 0 ] ) . toEqual ( 1 / ( sellOffers [ 0 ] . getPrice ( ) / priceDivisor ) ) ;
expect ( marketDepth . getSellPricesList ( ) [ 1 ] ) . toEqual ( 1 / ( sellOffers [ 1 ] . getPrice ( ) / priceDivisor ) ) ;
expect ( marketDepth . getSellPricesList ( ) [ 2 ] ) . toEqual ( 1 / ( sellOffers [ 2 ] . getPrice ( ) / priceDivisor ) ) ;
expect ( marketDepth . getSellDepthList ( ) [ 0 ] ) . toEqual ( 0.3 ) ;
expect ( marketDepth . getSellDepthList ( ) [ 1 ] ) . toEqual ( 0.6 ) ;
expect ( marketDepth . getSellDepthList ( ) [ 2 ] ) . toEqual ( 1 ) ;
// clear offers
2022-02-16 10:09:59 -05:00
await clearOffers ( alice , assetCode ) ;
await clearOffers ( bob , assetCode ) ;
2022-02-11 17:13:56 -06:00
// test invalid currency
await expect ( async ( ) = > { await alice . getMarketDepth ( "INVALID_CURRENCY" ) } )
. rejects
. toThrow ( 'Currency not found: INVALID_CURRENCY' ) ;
} ) ;
2022-02-09 01:41:00 -08:00
test ( "Can register as dispute agents" , async ( ) = > {
await arbitrator . registerDisputeAgent ( "mediator" , TestConfig . devPrivilegePrivKey ) ; // TODO: bisq mediator = haveno arbitrator
await arbitrator . registerDisputeAgent ( "refundagent" , TestConfig . devPrivilegePrivKey ) ; // TODO: no refund agent in haveno
// test bad dispute agent type
try {
await arbitrator . registerDisputeAgent ( "unsupported type" , TestConfig . devPrivilegePrivKey ) ;
throw new Error ( "should have thrown error registering bad type" ) ;
} catch ( err ) {
if ( err . message !== "unknown dispute agent type 'unsupported type'" ) throw new Error ( "Unexpected error: " + err . message ) ;
}
// test bad key
try {
await arbitrator . registerDisputeAgent ( "mediator" , "bad key" ) ;
throw new Error ( "should have thrown error registering bad key" ) ;
} catch ( err ) {
if ( err . message !== "invalid registration key" ) throw new Error ( "Unexpected error: " + err . message ) ;
}
} ) ;
2021-09-14 08:27:45 -04:00
test ( "Can get offers" , async ( ) = > {
2022-02-16 10:09:59 -05:00
for ( let assetCode of TestConfig . assetCodes ) {
let offers : OfferInfo [ ] = await alice . getOffers ( assetCode ) ;
for ( let offer of offers ) testOffer ( offer ) ;
2021-09-12 09:39:21 -04:00
}
} ) ;
2021-09-17 09:33:58 -04:00
test ( "Can get my offers" , async ( ) = > {
2022-02-16 10:09:59 -05:00
for ( let assetCode of TestConfig . assetCodes ) {
let offers : OfferInfo [ ] = await alice . getMyOffers ( assetCode ) ;
for ( let offer of offers ) testOffer ( offer ) ;
}
} ) ;
test ( "Can get payment methods" , async ( ) = > {
let paymentMethods : PaymentMethod [ ] = await alice . getPaymentMethods ( ) ;
expect ( paymentMethods . length ) . toBeGreaterThan ( 0 ) ;
for ( let paymentMethod of paymentMethods ) {
expect ( paymentMethod . getId ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( BigInt ( paymentMethod . getMaxTradeLimit ( ) ) ) . toBeGreaterThan ( BigInt ( 0 ) ) ;
expect ( BigInt ( paymentMethod . getMaxTradePeriod ( ) ) ) . toBeGreaterThan ( BigInt ( 0 ) ) ;
2021-09-14 08:27:45 -04:00
}
} ) ;
test ( "Can get payment accounts" , async ( ) = > {
2021-09-17 09:33:58 -04:00
let paymentAccounts : PaymentAccount [ ] = await alice . getPaymentAccounts ( ) ;
2021-09-14 08:27:45 -04:00
for ( let paymentAccount of paymentAccounts ) {
2021-11-16 08:06:20 -05:00
if ( paymentAccount . getPaymentAccountPayload ( ) ! . getCryptoCurrencyAccountPayload ( ) ) { // TODO (woodser): test non-crypto
testCryptoPaymentAccount ( paymentAccount ) ;
}
2021-09-14 08:27:45 -04:00
}
} ) ;
2022-02-16 10:09:59 -05:00
test ( "Can create fiat payment accounts" , async ( ) = > {
// get payment account form
const paymentMethodId = 'REVOLUT' ;
let accountForm = await alice . getPaymentAccountForm ( paymentMethodId ) ;
// edit form
accountForm . accountName = "Revolut account " + GenUtils . getUUID ( ) ;
accountForm . userName = "user123" ;
// create payment account
let fiatAccount = await alice . createPaymentAccount ( accountForm ) ;
expect ( fiatAccount . getAccountName ( ) ) . toEqual ( accountForm . accountName ) ;
expect ( fiatAccount . getTradeCurrenciesList ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( fiatAccount . getPaymentAccountPayload ( ) ! . getPaymentMethodId ( ) ) . toEqual ( paymentMethodId ) ;
expect ( fiatAccount . getPaymentAccountPayload ( ) ! . getRevolutAccountPayload ( ) ! . getAccountId ( ) ) . toEqual ( accountForm . userName ) ;
expect ( fiatAccount . getPaymentAccountPayload ( ) ! . getRevolutAccountPayload ( ) ! . getUserName ( ) ) . toEqual ( accountForm . userName ) ;
// payment account added
let found = false ;
for ( let paymentAccount of await alice . getPaymentAccounts ( ) ) {
if ( paymentAccount . getId ( ) === fiatAccount . getId ( ) ) {
found = true ;
break ;
}
}
assert ( found , "Payment account not found after adding" ) ;
} ) ;
2021-10-22 13:51:57 -04:00
test ( "Can create crypto payment accounts" , async ( ) = > {
2022-02-11 17:13:56 -06:00
2022-02-16 10:09:59 -05:00
// test each crypto
for ( let testAccount of TestConfig . cryptoAddresses ) {
2021-10-22 13:51:57 -04:00
// create payment account
let name = testAccount . currencyCode + " " + testAccount . address . substr ( 0 , 8 ) + "... " + GenUtils . getUUID ( ) ;
2022-02-16 10:09:59 -05:00
let paymentAccount : PaymentAccount = await alice . createCryptoPaymentAccount ( name , testAccount . currencyCode , testAccount . address ) ;
2021-10-22 13:51:57 -04:00
testCryptoPaymentAccount ( paymentAccount ) ;
testCryptoPaymentAccountEquals ( paymentAccount , testAccount , name ) ;
// fetch and test payment account
let fetchedAccount : PaymentAccount | undefined ;
for ( let account of await alice . getPaymentAccounts ( ) ) {
if ( paymentAccount . getId ( ) === account . getId ( ) ) {
fetchedAccount = account ;
break ;
}
2021-10-15 13:17:52 -04:00
}
2021-10-22 13:51:57 -04:00
if ( ! fetchedAccount ) throw new Error ( "Payment account not found after being added" ) ;
testCryptoPaymentAccount ( paymentAccount ) ;
testCryptoPaymentAccountEquals ( fetchedAccount , testAccount , name ) ;
}
2022-04-11 17:52:21 -04:00
// TODO (woodser): update from latest Bisq CorePaymentAccountService.java for currency and address validation
/* / / test invalid currency code
await expect ( async ( ) = > { await alice . createCryptoPaymentAccount ( "My account" , "ABC" , "123" ) ; } )
. rejects
. toThrow ( 'Unsupported cryptocurrency code: ABC' ) ;
// test invalid address
await expect ( async ( ) = > { await alice . createCryptoPaymentAccount ( "My account" , "ETH" , "123" ) ; } )
. rejects
. toThrow ( 'Invalid address' ) ; * /
// TODO (woodser): test rejecting account with duplicate name
2021-10-22 13:51:57 -04:00
function testCryptoPaymentAccountEquals ( paymentAccount : PaymentAccount , testAccount : any , name : string ) {
expect ( paymentAccount . getAccountName ( ) ) . toEqual ( name ) ;
expect ( paymentAccount . getPaymentAccountPayload ( ) ! . getCryptoCurrencyAccountPayload ( ) ! . getAddress ( ) ) . toEqual ( testAccount . address ) ;
expect ( paymentAccount . getSelectedTradeCurrency ( ) ! . getCode ( ) ) . toEqual ( testAccount . currencyCode . toUpperCase ( ) ) ;
2021-10-15 13:17:52 -04:00
}
2021-09-14 08:30:22 -04:00
} ) ;
test ( "Can post and remove an offer" , async ( ) = > {
2022-02-11 17:13:56 -06:00
2021-11-12 10:26:22 -05:00
// wait for alice to have unlocked balance to post offer
2021-10-15 13:17:52 -04:00
let tradeAmount : bigint = BigInt ( "250000000000" ) ;
2021-11-19 17:25:49 -05:00
await waitForUnlockedBalance ( tradeAmount * BigInt ( "2" ) , alice ) ;
2021-09-17 09:33:58 -04:00
// get unlocked balance before reserving funds for offer
let unlockedBalanceBefore : bigint = BigInt ( ( await alice . getBalances ( ) ) . getUnlockedBalance ( ) ) ;
// post offer
2022-02-16 10:09:59 -05:00
let assetCode = getRandomAssetCode ( ) ;
let offer : OfferInfo = await postOffer ( alice , { assetCode : assetCode } ) ;
if ( isCrypto ( assetCode ) ) assert . equal ( offer . getBaseCurrencyCode ( ) , assetCode ) ; // TODO: crypto base/counter is inverted
else assert . equal ( offer . getCounterCurrencyCode ( ) , assetCode ) ;
2021-12-14 13:04:02 -05:00
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
// has offer
offer = await alice . getMyOffer ( offer . getId ( ) ) ;
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2021-09-17 09:33:58 -04:00
// cancel offer
await alice . removeOffer ( offer . getId ( ) ) ;
// offer is removed from my offers
2022-02-16 10:09:59 -05:00
if ( getOffer ( await alice . getMyOffers ( assetCode , "buy" ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in my offers after removal" ) ;
2021-09-17 09:33:58 -04:00
// reserved balance released
2022-04-06 11:28:56 -04:00
expect ( BigInt ( ( await alice . getBalances ( ) ) . getUnlockedBalance ( ) ) ) . toEqual ( unlockedBalanceBefore ) ;
2021-09-17 09:33:58 -04:00
} ) ;
2022-04-08 18:33:37 -04:00
test ( "Can prepare for trading" , async ( ) = > {
// create ethereum and revolut payment accounts
await createPaymentAccount ( alice , "eth" ) ;
await createRevolutPaymentAccount ( alice ) ;
await createPaymentAccount ( bob , "eth" ) ;
await createRevolutPaymentAccount ( bob ) ;
// fund alice and bob with at least 5 outputs of 0.5 XMR
let numOutputs = 5 ;
let outputAmt = BigInt ( "500000000000" ) ;
let walletsToFund = [ ] ;
if ( ! await hasUnlockedOutputs ( aliceWallet , outputAmt , numOutputs ) ) walletsToFund . push ( aliceWallet ) ;
if ( ! await hasUnlockedOutputs ( bobWallet , outputAmt , numOutputs ) ) walletsToFund . push ( bobWallet ) ;
await fundWallets ( walletsToFund , outputAmt , numOutputs ) ;
} ) ;
2022-02-16 10:09:59 -05:00
// TODO (woodser): test grpc notifications
test ( "Can complete a trade" , async ( ) = > {
// wait for alice and bob to have unlocked balance for trade
let tradeAmount : bigint = BigInt ( "250000000000" ) ;
await waitForUnlockedBalance ( tradeAmount * BigInt ( "2" ) , alice , bob ) ;
let aliceBalancesBefore = await alice . getBalances ( ) ;
let bobBalancesBefore : XmrBalanceInfo = await bob . getBalances ( ) ;
// register to receive notifications
let aliceNotifications : NotificationMessage [ ] = [ ] ;
let bobNotifications : NotificationMessage [ ] = [ ] ;
await alice . addNotificationListener ( notification = > { aliceNotifications . push ( notification ) ; } ) ;
await bob . addNotificationListener ( notification = > { bobNotifications . push ( notification ) ; } ) ;
// alice posts offer
let assetCode = getRandomAssetCode ( ) ;
let direction = "buy" ;
HavenoUtils . log ( 1 , "Alice posting offer to " + direction + " XMR for " + assetCode ) ;
let offer : OfferInfo = await postOffer ( alice , { direction : direction , amount : tradeAmount , assetCode : assetCode } ) ;
expect ( offer . getState ( ) ) . toEqual ( "AVAILABLE" ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Alice done posting offer " + offer . getId ( ) ) ;
2022-02-16 10:09:59 -05:00
// TODO (woodser): test error message taking offer before posted
// bob sees offer
await wait ( TestConfig . walletSyncPeriodMs * 2 ) ;
let offerBob = getOffer ( await bob . getOffers ( assetCode , direction ) , offer . getId ( ) ) ;
if ( ! offerBob ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in peer's offers after posting" ) ;
expect ( offerBob . getState ( ) ) . toEqual ( "UNKNOWN" ) ; // TODO: offer state is not known?
// cannot take offer with invalid payment id
let aliceTradesBefore = await alice . getTrades ( ) ;
let bobTradesBefore = await bob . getTrades ( ) ;
try {
await bob . takeOffer ( offer . getId ( ) , "abc" ) ;
throw new Error ( "taking offer with invalid payment account id should fail" ) ;
} catch ( err ) {
assert . equal ( err . message , "payment account with id 'abc' not found" ) ;
assert . equal ( ( await alice . getTrades ( ) ) . length , aliceTradesBefore . length , "alice should not have new trades" ) ;
assert . equal ( ( await bob . getTrades ( ) ) . length , bobTradesBefore . length , "bob should not have new trades" ) ; // TODO (woodser): also test balance unreserved
}
// bob creates random payment account
let paymentAccount = await createPaymentAccount ( bob , assetCode ) ;
// bob takes offer
let startTime = Date . now ( ) ;
HavenoUtils . log ( 1 , "Bob taking offer" ) ;
2022-04-05 14:34:19 -04:00
let trade : TradeInfo = await bob . takeOffer ( offer . getId ( ) , paymentAccount . getId ( ) ) ;
2022-02-16 10:09:59 -05:00
expect ( trade . getPhase ( ) ) . toEqual ( "DEPOSIT_PUBLISHED" ) ;
HavenoUtils . log ( 1 , "Bob done taking offer in " + ( Date . now ( ) - startTime ) + " ms" ) ;
// alice is notified that offer is taken
2022-04-06 11:28:56 -04:00
await wait ( 1000 ) ;
2022-02-16 10:09:59 -05:00
let tradeNotifications = getNotifications ( aliceNotifications , NotificationMessage . NotificationType . TRADE_UPDATE ) ;
expect ( tradeNotifications . length ) . toBe ( 1 ) ;
expect ( tradeNotifications [ 0 ] . getTrade ( ) ! . getPhase ( ) ) . toEqual ( "DEPOSIT_PUBLISHED" ) ;
expect ( tradeNotifications [ 0 ] . getTitle ( ) ) . toEqual ( "Offer Taken" ) ;
expect ( tradeNotifications [ 0 ] . getMessage ( ) ) . toEqual ( "Your offer " + offer . getId ( ) + " has been accepted" ) ;
// alice is notified of balance change
// bob can get trade
let fetchedTrade : TradeInfo = await bob . getTrade ( trade . getTradeId ( ) ) ;
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "DEPOSIT_PUBLISHED" ) ;
// TODO: test fetched trade
// test bob's balances after taking trade
let bobBalancesAfter : XmrBalanceInfo = await bob . getBalances ( ) ;
expect ( BigInt ( bobBalancesAfter . getUnlockedBalance ( ) ) ) . toBeLessThan ( BigInt ( bobBalancesBefore . getUnlockedBalance ( ) ) ) ;
expect ( BigInt ( bobBalancesAfter . getReservedOfferBalance ( ) ) + BigInt ( bobBalancesAfter . getReservedTradeBalance ( ) ) ) . toBeGreaterThan ( BigInt ( bobBalancesBefore . getReservedOfferBalance ( ) ) + BigInt ( bobBalancesBefore . getReservedTradeBalance ( ) ) ) ;
// bob is notified of balance change
// alice can get trade
fetchedTrade = await alice . getTrade ( trade . getTradeId ( ) ) ;
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "DEPOSIT_PUBLISHED" ) ;
2022-03-09 04:43:30 -08:00
// test trader chat
await testTradeChat ( trade . getTradeId ( ) , alice , bob ) ;
2022-02-16 10:09:59 -05:00
// mine until deposit txs unlock
await waitForUnlockedTxs ( fetchedTrade . getMakerDepositTxId ( ) , fetchedTrade . getTakerDepositTxId ( ) ) ;
// alice notified to send payment
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxWalletStartupMs + TestConfig . walletSyncPeriodMs * 2 ) ;
2022-02-16 10:09:59 -05:00
fetchedTrade = await alice . getTrade ( trade . getTradeId ( ) ) ;
2022-04-05 18:26:16 -04:00
expect ( fetchedTrade . getIsDepositUnlocked ( ) ) . toBe ( true ) ;
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "DEPOSIT_UNLOCKED" ) ;
2022-02-16 10:09:59 -05:00
fetchedTrade = await bob . getTrade ( trade . getTradeId ( ) ) ;
2022-04-05 18:26:16 -04:00
expect ( fetchedTrade . getIsDepositUnlocked ( ) ) . toBe ( true ) ;
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "DEPOSIT_UNLOCKED" ) ;
2022-02-16 10:09:59 -05:00
// alice indicates payment is sent
2022-04-05 18:26:16 -04:00
HavenoUtils . log ( 1 , "Alice confirming payment sent" ) ;
2022-02-16 10:09:59 -05:00
await alice . confirmPaymentStarted ( trade . getTradeId ( ) ) ;
fetchedTrade = await alice . getTrade ( trade . getTradeId ( ) ) ;
2022-04-06 11:28:56 -04:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "PAYMENT_SENT" ) ;
2022-02-16 10:09:59 -05:00
// bob notified payment is sent
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxTimePeerNoticeMs + TestConfig . maxWalletStartupMs ) ;
2022-02-16 10:09:59 -05:00
fetchedTrade = await bob . getTrade ( trade . getTradeId ( ) ) ;
2022-04-06 11:28:56 -04:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "PAYMENT_SENT" ) ;
2022-02-16 10:09:59 -05:00
// bob confirms payment is received
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Bob confirming payment received" ) ;
2022-02-16 10:09:59 -05:00
await bob . confirmPaymentReceived ( trade . getTradeId ( ) ) ;
fetchedTrade = await bob . getTrade ( trade . getTradeId ( ) ) ;
2022-04-07 10:53:58 -04:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "PAYMENT_RECEIVED" ) ; // TODO (woodser): may be PAYOUT_PUBLISHED if seller sends multisig info after confirmation
2022-02-16 10:09:59 -05:00
// alice notified trade is complete and of balance changes
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxWalletStartupMs + TestConfig . walletSyncPeriodMs * 2 ) ;
2022-02-16 10:09:59 -05:00
fetchedTrade = await alice . getTrade ( trade . getTradeId ( ) ) ;
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "PAYOUT_PUBLISHED" ) ;
// test balances after payout tx
let aliceBalancesAfter = await alice . getBalances ( ) ;
bobBalancesAfter = await bob . getBalances ( ) ;
let aliceFee = BigInt ( aliceBalancesBefore . getBalance ( ) ) + tradeAmount - BigInt ( aliceBalancesAfter . getBalance ( ) ) ;
let bobFee = BigInt ( bobBalancesBefore . getBalance ( ) ) - tradeAmount - BigInt ( bobBalancesAfter . getBalance ( ) ) ;
expect ( aliceFee ) . toBeLessThanOrEqual ( TestConfig . maxFee ) ;
expect ( aliceFee ) . toBeGreaterThan ( BigInt ( "0" ) ) ;
expect ( bobFee ) . toBeLessThanOrEqual ( TestConfig . maxFee ) ;
expect ( bobFee ) . toBeGreaterThan ( BigInt ( "0" ) ) ;
} ) ;
2022-03-07 09:57:00 -08:00
test ( "Can resolve disputes" , async ( ) = > {
// wait for alice and bob to have unlocked balance for trade
let tradeAmount : bigint = BigInt ( "250000000000" ) ;
2022-04-06 11:28:56 -04:00
await fundWallets ( [ aliceWallet , bobWallet ] , tradeAmount * BigInt ( "6" ) , 4 ) ;
2022-03-07 09:57:00 -08:00
// register to receive notifications
let aliceNotifications : NotificationMessage [ ] = [ ] ;
let bobNotifications : NotificationMessage [ ] = [ ] ;
let arbitratorNotifications : NotificationMessage [ ] = [ ] ;
2022-04-06 11:28:56 -04:00
await alice . addNotificationListener ( notification = > { HavenoUtils . log ( 3 , "Alice received notification " + notification . getType ( ) + " " + ( notification . getChatMessage ( ) ? notification . getChatMessage ( ) ? . getMessage ( ) : "" ) ) ; aliceNotifications . push ( notification ) ; } ) ;
await bob . addNotificationListener ( notification = > { HavenoUtils . log ( 3 , "Bob received notification " + notification . getType ( ) + " " + ( notification . getChatMessage ( ) ? notification . getChatMessage ( ) ? . getMessage ( ) : "" ) ) ; bobNotifications . push ( notification ) ; } ) ;
await arbitrator . addNotificationListener ( notification = > { HavenoUtils . log ( 3 , "Arbitrator received notification " + notification . getType ( ) + " " + ( notification . getChatMessage ( ) ? notification . getChatMessage ( ) ? . getMessage ( ) : "" ) ) ; arbitratorNotifications . push ( notification ) ; } ) ;
// TODO: notification collector with logging
2022-03-07 09:57:00 -08:00
// alice posts offers to buy xmr
let numOffers = 4 ;
HavenoUtils . log ( 1 , "Alice posting offers" ) ;
let direction = "buy" ;
let offers = [ ] ;
2022-04-06 11:28:56 -04:00
for ( let i = 0 ; i < numOffers ; i ++ ) offers . push ( postOffer ( alice , { direction : direction , amount : tradeAmount , awaitUnlockedBalance : true } ) ) ;
2022-03-07 09:57:00 -08:00
offers = await Promise . all ( offers ) ;
HavenoUtils . log ( 1 , "Alice done posting offers" ) ;
2022-04-06 11:28:56 -04:00
for ( let i = 0 ; i < offers . length ; i ++ ) HavenoUtils . log ( 2 , "Offer " + i + ": " + ( await alice . getMyOffer ( offers [ i ] . getId ( ) ) ) . getId ( ) ) ;
2022-03-07 09:57:00 -08:00
// wait for offers to post
await wait ( TestConfig . walletSyncPeriodMs * 2 ) ;
// bob takes offers
let paymentAccount = await createPaymentAccount ( bob , "eth" ) ;
HavenoUtils . log ( 1 , "Bob taking offers" ) ;
let trades = [ ] ;
2022-04-06 11:28:56 -04:00
for ( let i = 0 ; i < numOffers ; i ++ ) trades . push ( bob . takeOffer ( offers [ i ] . getId ( ) , paymentAccount . getId ( ) ) ) ;
trades = await Promise . all ( trades ) ;
HavenoUtils . log ( 1 , "Bob done taking offers" ) ;
2022-03-07 09:57:00 -08:00
// test trades
let depositTxIds : string [ ] = [ ] ;
for ( let trade of trades ) {
2022-04-06 11:28:56 -04:00
if ( trade . getPhase ( ) !== "DEPOSIT_PUBLISHED" ) throw new Error ( "Trade phase expected to be DEPOSIT_PUBLISHED but was " + trade . getPhase ( ) + " for trade " + trade . getTradeId ( ) ) ;
2022-03-07 09:57:00 -08:00
expect ( trade . getPhase ( ) ) . toEqual ( "DEPOSIT_PUBLISHED" ) ;
let fetchedTrade : TradeInfo = await bob . getTrade ( trade . getTradeId ( ) ) ;
2022-04-06 11:28:56 -04:00
if ( fetchedTrade . getPhase ( ) !== "DEPOSIT_PUBLISHED" ) throw new Error ( "Fetched phase expected to be DEPOSIT_PUBLISHED but was " + fetchedTrade . getPhase ( ) + " for trade " + fetchedTrade . getTradeId ( ) ) ;
2022-03-07 09:57:00 -08:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "DEPOSIT_PUBLISHED" ) ;
depositTxIds . push ( fetchedTrade . getMakerDepositTxId ( ) ) ;
depositTxIds . push ( fetchedTrade . getTakerDepositTxId ( ) ) ;
}
// mine until deposit txs unlock
await waitForUnlockedTxs ( . . . depositTxIds ) ;
// open disputes
HavenoUtils . log ( 1 , "Opening disputes" ) ;
2022-04-06 11:28:56 -04:00
await Promise . all ( [
bob . openDispute ( trades [ 0 ] . getTradeId ( ) ) ,
alice . openDispute ( trades [ 1 ] . getTradeId ( ) ) ,
bob . openDispute ( trades [ 2 ] . getTradeId ( ) ) ,
alice . openDispute ( trades [ 3 ] . getTradeId ( ) )
] ) ;
HavenoUtils . log ( 1 , "Done opening disputes" ) ;
// test dispute state
2022-03-07 09:57:00 -08:00
let bobDispute = await bob . getDispute ( trades [ 0 ] . getTradeId ( ) ) ;
expect ( bobDispute . getTradeId ( ) ) . toEqual ( trades [ 0 ] . getTradeId ( ) ) ;
expect ( bobDispute . getIsOpener ( ) ) . toBe ( true ) ;
expect ( bobDispute . getDisputeOpenerIsBuyer ( ) ) . toBe ( false ) ;
// get non-existing dispute should fail
try {
await bob . getDispute ( "invalid" ) ;
throw new Error ( "get dispute with invalid id should fail" ) ;
} catch ( err ) {
assert . equal ( err . message , "dispute for trade id 'invalid' not found" ) ;
}
// alice sees the dispute
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxTimePeerNoticeMs + TestConfig . maxWalletStartupMs ) ;
2022-03-07 09:57:00 -08:00
let aliceDispute = await alice . getDispute ( trades [ 0 ] . getTradeId ( ) ) ;
expect ( aliceDispute . getTradeId ( ) ) . toEqual ( trades [ 0 ] . getTradeId ( ) ) ;
expect ( aliceDispute . getIsOpener ( ) ) . toBe ( false ) ;
// arbitrator sees both disputes
let disputes = await arbitrator . getDisputes ( ) ;
expect ( disputes . length ) . toBeGreaterThanOrEqual ( 2 ) ;
let arbAliceDispute = disputes . find ( d = > d . getId ( ) === aliceDispute . getId ( ) ) ;
assert ( arbAliceDispute ) ;
let arbBobDispute = disputes . find ( d = > d . getId ( ) === bobDispute . getId ( ) ) ;
assert ( arbBobDispute ) ;
// arbitrator sends chat messages to alice and bob
HavenoUtils . log ( 1 , "Testing chat messages" ) ;
await arbitrator . sendDisputeChatMessage ( arbBobDispute ! . getId ( ) , "Arbitrator chat message to Bob" , [ ] ) ;
await arbitrator . sendDisputeChatMessage ( arbAliceDispute ! . getId ( ) , "Arbitrator chat message to Alice" , [ ] ) ;
// alice and bob reply to arbitrator chat messages
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxTimePeerNoticeMs ) ; // wait for arbitrator's message to arrive
2022-03-07 09:57:00 -08:00
let attachment = new Attachment ( ) ;
let bytes = new Uint8Array ( Buffer . from ( "Proof Bob was scammed" , "utf8" ) ) ;
attachment . setBytes ( bytes ) ;
attachment . setFileName ( "proof.txt" ) ;
let attachment2 = new Attachment ( ) ;
let bytes2 = new Uint8Array ( Buffer . from ( "picture bytes" , "utf8" ) ) ;
attachment2 . setBytes ( bytes2 ) ;
attachment2 . setFileName ( "proof.png" ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 2 , "Bob sending chat message to arbitrator. tradeId=" + trades [ 0 ] . getTradeId ( ) + ", disputeId=" + bobDispute . getId ( ) ) ;
2022-03-07 09:57:00 -08:00
await bob . sendDisputeChatMessage ( bobDispute . getId ( ) , "Bob chat message" , [ attachment , attachment2 ] ) ;
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxTimePeerNoticeMs ) ; // wait for bob's message to arrive
HavenoUtils . log ( 2 , "Alice sending chat message to arbitrator. tradeId=" + trades [ 0 ] . getTradeId ( ) + ", disputeId=" + aliceDispute . getId ( ) ) ;
2022-03-07 09:57:00 -08:00
await alice . sendDisputeChatMessage ( aliceDispute . getId ( ) , "Alice chat message" , [ ] ) ;
// test alice and bob's chat messages
await wait ( TestConfig . maxTimePeerNoticeMs ) ;
let updatedDispute = await bob . getDispute ( trades [ 0 ] . getTradeId ( ) ) ;
let messages = updatedDispute . getChatMessageList ( ) ;
expect ( messages . length ) . toEqual ( 3 ) ; // 1st message is the system message
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( "Arbitrator chat message to Bob" ) ;
expect ( messages [ 2 ] . getMessage ( ) ) . toEqual ( "Bob chat message" ) ;
let attachments = messages [ 2 ] . getAttachmentsList ( ) ;
expect ( attachments . length ) . toEqual ( 2 ) ;
expect ( attachments [ 0 ] . getFileName ( ) ) . toEqual ( "proof.txt" ) ;
expect ( attachments [ 0 ] . getBytes ( ) ) . toEqual ( bytes ) ;
expect ( attachments [ 1 ] . getFileName ( ) ) . toEqual ( "proof.png" ) ;
expect ( attachments [ 1 ] . getBytes ( ) ) . toEqual ( bytes2 ) ;
updatedDispute = await alice . getDispute ( trades [ 0 ] . getTradeId ( ) ) ;
messages = updatedDispute . getChatMessageList ( ) ;
expect ( messages . length ) . toEqual ( 3 ) ;
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( "Arbitrator chat message to Alice" ) ;
expect ( messages [ 2 ] . getMessage ( ) ) . toEqual ( "Alice chat message" ) ;
// test notifications of chat messages
let chatNotifications = getNotifications ( aliceNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( "Arbitrator chat message to Alice" ) ;
chatNotifications = getNotifications ( bobNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( "Arbitrator chat message to Bob" ) ;
// arbitrator has 2 chat messages, one with attachments
chatNotifications = getNotifications ( arbitratorNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
expect ( chatNotifications . length ) . toBe ( 2 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( "Bob chat message" ) ;
assert ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getAttachmentsList ( ) ) ;
attachments = chatNotifications [ 0 ] . getChatMessage ( ) ? . getAttachmentsList ( ) ! ;
expect ( attachments [ 0 ] . getFileName ( ) ) . toEqual ( "proof.txt" ) ;
expect ( attachments [ 0 ] . getBytes ( ) ) . toEqual ( bytes ) ;
expect ( attachments [ 1 ] . getFileName ( ) ) . toEqual ( "proof.png" ) ;
expect ( attachments [ 1 ] . getBytes ( ) ) . toEqual ( bytes2 ) ;
expect ( chatNotifications [ 1 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( "Alice chat message" ) ;
// award trade amount to seller
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Awarding trade amount to seller, trade " + trades [ 0 ] . getTradeId ( ) ) ;
2022-03-07 09:57:00 -08:00
let bobBalancesBefore = await bob . getBalances ( ) ;
let aliceBalancesBefore = await alice . getBalances ( ) ;
await arbitrator . resolveDispute ( trades [ 0 ] . getTradeId ( ) , DisputeResult . Winner . SELLER , DisputeResult . Reason . PEER_WAS_LATE , "Seller is winner" ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Done resolving dispute" ) ;
2022-03-07 09:57:00 -08:00
// dispute is resolved
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxWalletStartupMs ) ;
2022-03-07 09:57:00 -08:00
updatedDispute = await alice . getDispute ( trades [ 0 ] . getTradeId ( ) ) ;
expect ( updatedDispute . getIsClosed ( ) ) . toBe ( true ) ;
updatedDispute = await bob . getDispute ( trades [ 0 ] . getTradeId ( ) ) ;
expect ( updatedDispute . getIsClosed ( ) ) . toBe ( true ) ;
// check balances after payout tx
await wait ( TestConfig . walletSyncPeriodMs * 2 ) ;
let aliceBalancesAfter = await alice . getBalances ( ) ;
let bobBalancesAfter = await bob . getBalances ( ) ;
let aliceDifference = BigInt ( aliceBalancesAfter . getBalance ( ) ) - BigInt ( aliceBalancesBefore . getBalance ( ) ) ;
let bobDifference = BigInt ( bobBalancesAfter . getBalance ( ) ) - BigInt ( bobBalancesBefore . getBalance ( ) ) ;
let winnerPayout = tradeAmount + HavenoUtils . centinerosToAtomicUnits ( offers [ 0 ] . getSellerSecurityDeposit ( ) ) ;
let loserPayout = HavenoUtils . centinerosToAtomicUnits ( offers [ 0 ] . getBuyerSecurityDeposit ( ) ) ;
expect ( loserPayout - aliceDifference ) . toBeLessThan ( TestConfig . maxFee ) ;
expect ( bobDifference ) . toEqual ( winnerPayout ) ;
// award trade amount to buyer
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Awarding trade amount to buyer, trade " + trades [ 1 ] . getTradeId ( ) ) ;
2022-03-07 09:57:00 -08:00
aliceBalancesBefore = await alice . getBalances ( ) ;
bobBalancesBefore = await bob . getBalances ( ) ;
await arbitrator . resolveDispute ( trades [ 1 ] . getTradeId ( ) , DisputeResult . Winner . BUYER , DisputeResult . Reason . SELLER_NOT_RESPONDING , "Buyer is winner" ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Done resolving dispute" ) ;
await wait ( TestConfig . maxTimePeerNoticeMs + TestConfig . maxWalletStartupMs * 2 + TestConfig . walletSyncPeriodMs ) ; // TODO (woodser): arbitrator sends mailbox message to trader -> trader opens and syncs multisig wallet and sends updated multisig hex to arbitrator -> arbitrator opens and syncs multisig wallet, signs payout tx and sends to trader -> trader finishes signing payout tx and broadcasts. more efficient way? traders can verify payout tx without syncing multisig wallet again
2022-03-07 09:57:00 -08:00
aliceBalancesAfter = await alice . getBalances ( ) ;
bobBalancesAfter = await bob . getBalances ( ) ;
aliceDifference = BigInt ( aliceBalancesAfter . getBalance ( ) ) - BigInt ( aliceBalancesBefore . getBalance ( ) ) ;
bobDifference = BigInt ( bobBalancesAfter . getBalance ( ) ) - BigInt ( bobBalancesBefore . getBalance ( ) ) ;
winnerPayout = tradeAmount + HavenoUtils . centinerosToAtomicUnits ( offers [ 1 ] . getBuyerSecurityDeposit ( ) ) ;
loserPayout = HavenoUtils . centinerosToAtomicUnits ( offers [ 1 ] . getSellerSecurityDeposit ( ) ) ;
2022-04-06 11:28:56 -04:00
if ( aliceDifference !== winnerPayout || loserPayout - bobDifference > TestConfig . maxFee ) {
HavenoUtils . log ( 0 , "WARNING: payout not observed. waiting longer" ) ; // TODO (woodser): refactor dispute resolution
await wait ( TestConfig . maxWalletStartupMs + TestConfig . walletSyncPeriodMs ) ;
aliceBalancesAfter = await alice . getBalances ( ) ;
bobBalancesAfter = await bob . getBalances ( ) ;
aliceDifference = BigInt ( aliceBalancesAfter . getBalance ( ) ) - BigInt ( aliceBalancesBefore . getBalance ( ) ) ;
bobDifference = BigInt ( bobBalancesAfter . getBalance ( ) ) - BigInt ( bobBalancesBefore . getBalance ( ) ) ;
}
2022-03-07 09:57:00 -08:00
expect ( aliceDifference ) . toEqual ( winnerPayout ) ;
expect ( loserPayout - bobDifference ) . toBeLessThan ( TestConfig . maxFee ) ;
// award half of trade amount to buyer
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Awarding half of trade amount to buyer, trade " + trades [ 2 ] . getTradeId ( ) ) ;
2022-03-07 09:57:00 -08:00
let customWinnerAmount = tradeAmount / BigInt ( 2 ) + HavenoUtils . centinerosToAtomicUnits ( offers [ 2 ] . getBuyerSecurityDeposit ( ) ) ;
aliceBalancesBefore = await alice . getBalances ( ) ;
bobBalancesBefore = await bob . getBalances ( ) ;
await arbitrator . resolveDispute ( trades [ 2 ] . getTradeId ( ) , DisputeResult . Winner . BUYER , DisputeResult . Reason . WRONG_SENDER_ACCOUNT , "Split trade amount" , customWinnerAmount ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Done resolving dispute" ) ;
await wait ( TestConfig . maxTimePeerNoticeMs + TestConfig . maxWalletStartupMs * 2 + TestConfig . walletSyncPeriodMs ) ;
2022-03-07 09:57:00 -08:00
aliceBalancesAfter = await alice . getBalances ( ) ;
bobBalancesAfter = await bob . getBalances ( ) ;
aliceDifference = BigInt ( aliceBalancesAfter . getBalance ( ) ) - BigInt ( aliceBalancesBefore . getBalance ( ) ) ;
bobDifference = BigInt ( bobBalancesAfter . getBalance ( ) ) - BigInt ( bobBalancesBefore . getBalance ( ) ) ;
loserPayout = tradeAmount + HavenoUtils . centinerosToAtomicUnits ( offers [ 2 ] . getBuyerSecurityDeposit ( ) ) + HavenoUtils . centinerosToAtomicUnits ( offers [ 2 ] . getSellerSecurityDeposit ( ) ) - customWinnerAmount ;
2022-04-06 11:28:56 -04:00
if ( aliceDifference !== customWinnerAmount || loserPayout - bobDifference > TestConfig . maxFee ) {
HavenoUtils . log ( 0 , "WARNING: payout not observed. waiting longer" ) ;
await wait ( TestConfig . maxWalletStartupMs + TestConfig . walletSyncPeriodMs ) ;
aliceBalancesAfter = await alice . getBalances ( ) ;
bobBalancesAfter = await bob . getBalances ( ) ;
aliceDifference = BigInt ( aliceBalancesAfter . getBalance ( ) ) - BigInt ( aliceBalancesBefore . getBalance ( ) ) ;
bobDifference = BigInt ( bobBalancesAfter . getBalance ( ) ) - BigInt ( bobBalancesBefore . getBalance ( ) ) ;
}
2022-03-07 09:57:00 -08:00
expect ( aliceDifference ) . toEqual ( customWinnerAmount ) ;
2022-04-06 11:28:56 -04:00
expect ( loserPayout - bobDifference ) . toBeLessThanOrEqual ( TestConfig . maxFee ) ;
2022-03-07 09:57:00 -08:00
// award too little to loser
customWinnerAmount = tradeAmount + HavenoUtils . centinerosToAtomicUnits ( offers [ 3 ] . getBuyerSecurityDeposit ( ) ) + HavenoUtils . centinerosToAtomicUnits ( offers [ 3 ] . getSellerSecurityDeposit ( ) ) - BigInt ( "10000" ) ;
try {
await arbitrator . resolveDispute ( trades [ 3 ] . getTradeId ( ) , DisputeResult . Winner . SELLER , DisputeResult . Reason . TRADE_ALREADY_SETTLED , "Loser gets too little" , customWinnerAmount ) ;
throw new Error ( "Should have failed resolving dispute with insufficient loser payout" ) ;
} catch ( err ) {
assert . equal ( err . message , "Loser payout is too small to cover the mining fee" ) ;
}
// award full amount to seller
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Awarding full amount to seller, trade " + trades [ 3 ] . getTradeId ( ) ) ;
2022-03-07 09:57:00 -08:00
customWinnerAmount = tradeAmount + HavenoUtils . centinerosToAtomicUnits ( offers [ 3 ] . getBuyerSecurityDeposit ( ) ) + HavenoUtils . centinerosToAtomicUnits ( offers [ 3 ] . getSellerSecurityDeposit ( ) ) ;
aliceBalancesBefore = await alice . getBalances ( ) ;
bobBalancesBefore = await bob . getBalances ( ) ;
await arbitrator . resolveDispute ( trades [ 3 ] . getTradeId ( ) , DisputeResult . Winner . SELLER , DisputeResult . Reason . TRADE_ALREADY_SETTLED , "Seller gets everything" , customWinnerAmount ) ;
2022-04-06 11:28:56 -04:00
await wait ( TestConfig . maxTimePeerNoticeMs + TestConfig . maxWalletStartupMs * 2 + TestConfig . walletSyncPeriodMs ) ;
2022-03-07 09:57:00 -08:00
aliceBalancesAfter = await alice . getBalances ( ) ;
bobBalancesAfter = await bob . getBalances ( ) ;
aliceDifference = BigInt ( aliceBalancesAfter . getBalance ( ) ) - BigInt ( aliceBalancesBefore . getBalance ( ) ) ;
bobDifference = BigInt ( bobBalancesAfter . getBalance ( ) ) - BigInt ( bobBalancesBefore . getBalance ( ) ) ;
expect ( aliceDifference ) . toEqual ( BigInt ( 0 ) ) ;
2022-04-06 11:28:56 -04:00
if ( customWinnerAmount - bobDifference > TestConfig . maxFee ) {
HavenoUtils . log ( 0 , "WARNING: payout not observed. waiting longer" ) ;
await wait ( TestConfig . maxWalletStartupMs + TestConfig . walletSyncPeriodMs ) ;
aliceBalancesAfter = await alice . getBalances ( ) ;
bobBalancesAfter = await bob . getBalances ( ) ;
aliceDifference = BigInt ( aliceBalancesAfter . getBalance ( ) ) - BigInt ( aliceBalancesBefore . getBalance ( ) ) ;
bobDifference = BigInt ( bobBalancesAfter . getBalance ( ) ) - BigInt ( bobBalancesBefore . getBalance ( ) ) ;
}
expect ( customWinnerAmount - bobDifference ) . toBeLessThanOrEqual ( TestConfig . maxFee ) ;
2022-03-07 09:57:00 -08:00
} ) ;
2022-02-16 10:09:59 -05:00
test ( "Cannot make or take offer with insufficient unlocked funds" , async ( ) = > {
2022-04-07 16:35:48 -04:00
let charlie : HavenoClient | undefined ;
2022-02-16 10:09:59 -05:00
let err : any ;
try {
// start charlie
2022-04-05 15:18:36 -04:00
charlie = await initHaveno ( ) ;
2022-02-16 10:09:59 -05:00
// charlie creates ethereum payment account
let paymentAccount = await createCryptoPaymentAccount ( charlie ) ;
// charlie cannot make offer with insufficient funds
try {
await postOffer ( charlie , { paymentAccountId : paymentAccount.getId ( ) } ) ;
throw new Error ( "Should have failed making offer with insufficient funds" )
} catch ( err ) {
let errTyped = err as grpcWeb . RpcError ;
assert . equal ( errTyped . code , 2 ) ;
assert ( err . message . includes ( "not enough money" ) , "Unexpected error: " + err . message ) ;
}
// alice posts offer
2022-04-05 14:34:19 -04:00
let offers : OfferInfo [ ] = await alice . getMyOffers ( "ETH" ) ;
2022-02-16 10:09:59 -05:00
let offer : OfferInfo ;
if ( offers . length ) offer = offers [ 0 ] ;
else {
let tradeAmount : bigint = BigInt ( "250000000000" ) ;
await waitForUnlockedBalance ( tradeAmount * BigInt ( "2" ) , alice ) ;
offer = await postOffer ( alice , { amount : tradeAmount } ) ;
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
await wait ( TestConfig . walletSyncPeriodMs * 2 ) ;
}
// charlie cannot take offer with insufficient funds
try {
2022-04-05 14:34:19 -04:00
await charlie . takeOffer ( offer . getId ( ) , paymentAccount . getId ( ) ) ;
2022-02-16 10:09:59 -05:00
throw new Error ( "Should have failed taking offer with insufficient funds" )
} catch ( err ) {
let errTyped = err as grpcWeb . RpcError ;
assert ( errTyped . message . includes ( "not enough money" ) , "Unexpected error: " + errTyped . message ) ;
assert . equal ( errTyped . code , 2 ) ;
}
// charlie does not have trade
try {
await charlie . getTrade ( offer . getId ( ) ) ;
} catch ( err ) {
let errTyped = err as grpcWeb . RpcError ;
assert . equal ( errTyped . code , 3 ) ;
2022-04-05 14:34:19 -04:00
assert ( errTyped . message . includes ( "trade with id '" + offer . getId ( ) + "' not found" ) ) ;
2022-02-16 10:09:59 -05:00
}
} catch ( err2 ) {
err = err2 ;
}
// stop charlie
if ( charlie ) await releaseHavenoProcess ( charlie ) ;
// TODO: how to delete trader app folder at end of test?
if ( err ) throw err ;
} ) ;
2021-09-17 09:33:58 -04:00
test ( "Invalidates offers when reserved funds are spent" , async ( ) = > {
2021-12-14 13:04:02 -05:00
let err ;
let tx ;
try {
// wait for alice to have unlocked balance for trade
let tradeAmount : bigint = BigInt ( "250000000000" ) ;
await waitForUnlockedBalance ( tradeAmount * BigInt ( "2" ) , alice ) ;
2021-09-17 09:33:58 -04:00
2021-12-14 13:04:02 -05:00
// get frozen key images before posting offer
let frozenKeyImagesBefore = [ ] ;
for ( let frozenOutput of await aliceWallet . getOutputs ( { isFrozen : true } ) ) frozenKeyImagesBefore . push ( frozenOutput . getKeyImage ( ) . getHex ( ) ) ;
// post offer
await wait ( 1000 ) ;
2022-02-16 10:09:59 -05:00
let assetCode = getRandomAssetCode ( ) ;
let offer : OfferInfo = await postOffer ( alice , { assetCode : assetCode , amount : tradeAmount } ) ;
2021-12-14 13:04:02 -05:00
// get key images reserved by offer
let reservedKeyImages = [ ] ;
let frozenKeyImagesAfter = [ ] ;
for ( let frozenOutput of await aliceWallet . getOutputs ( { isFrozen : true } ) ) frozenKeyImagesAfter . push ( frozenOutput . getKeyImage ( ) . getHex ( ) ) ;
for ( let frozenKeyImageAfter of frozenKeyImagesAfter ) {
if ( ! frozenKeyImagesBefore . includes ( frozenKeyImageAfter ) ) reservedKeyImages . push ( frozenKeyImageAfter ) ;
}
// offer is available to peers
2021-12-16 20:10:40 -05:00
await wait ( TestConfig . walletSyncPeriodMs * 2 ) ;
2022-02-16 10:09:59 -05:00
if ( ! getOffer ( await bob . getOffers ( assetCode , "buy" ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in peer's offers after posting" ) ;
2021-12-14 13:04:02 -05:00
// spend one of offer's reserved outputs
if ( ! reservedKeyImages . length ) throw new Error ( "No reserved key images detected" ) ;
await aliceWallet . thawOutput ( reservedKeyImages [ 0 ] ) ;
tx = await aliceWallet . sweepOutput ( { keyImage : reservedKeyImages [ 0 ] , address : await aliceWallet . getPrimaryAddress ( ) , relay : false } ) ;
await monerod . submitTxHex ( tx . getFullHex ( ) , true ) ;
// wait for spend to be seen
2021-12-16 20:10:40 -05:00
await wait ( TestConfig . walletSyncPeriodMs * 2 ) ; // TODO (woodser): need place for common test utilities
2021-12-14 13:04:02 -05:00
// offer is removed from peer offers
2022-02-16 10:09:59 -05:00
if ( getOffer ( await bob . getOffers ( assetCode , "buy" ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in peer's offers after reserved funds spent" ) ;
2021-12-14 13:04:02 -05:00
// offer is removed from my offers
2022-02-16 10:09:59 -05:00
if ( getOffer ( await alice . getMyOffers ( assetCode , "buy" ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in my offers after reserved funds spent" ) ;
2021-12-14 13:04:02 -05:00
// offer is automatically cancelled
try {
await alice . removeOffer ( offer . getId ( ) ) ;
throw new Error ( "cannot remove invalidated offer" ) ;
} catch ( err ) {
2021-12-30 22:03:00 +02:00
if ( err . message === "cannot remove invalidated offer" ) throw new Error ( "Unexpected error: " + err . message ) ;
2021-12-14 13:04:02 -05:00
}
} catch ( err2 ) {
err = err2 ;
2021-09-17 09:33:58 -04:00
}
2021-12-14 13:04:02 -05:00
// flush tx from pool
if ( tx ) await monerod . flushTxPool ( tx . getHash ( ) ) ;
if ( err ) throw err ;
} ) ;
// TODO (woodser): test arbitrator state too
// TODO (woodser): test breaking protocol after depositing to multisig (e.g. don't send payment account payload by deleting it)
test ( "Handles unexpected errors during trade initialization" , async ( ) = > {
2022-04-07 16:35:48 -04:00
let traders : HavenoClient [ ] = [ ] ;
2021-12-14 13:04:02 -05:00
let err : any ;
2021-09-17 09:33:58 -04:00
try {
2021-12-14 13:04:02 -05:00
// start and fund 3 trader processes
2022-02-16 10:09:59 -05:00
HavenoUtils . log ( 1 , "Starting trader processes" ) ;
2022-04-05 15:18:36 -04:00
traders = await initHavenos ( 3 ) ;
2022-02-16 10:09:59 -05:00
HavenoUtils . log ( 1 , "Funding traders" ) ;
2021-12-15 12:41:00 -05:00
let tradeAmount : bigint = BigInt ( "250000000000" ) ;
2021-12-14 13:04:02 -05:00
await waitForUnlockedBalance ( tradeAmount * BigInt ( "2" ) , traders [ 0 ] , traders [ 1 ] , traders [ 2 ] ) ;
// trader 0 posts offer
2022-02-16 10:09:59 -05:00
HavenoUtils . log ( 1 , "Posting offer" ) ;
2022-02-11 17:13:56 -06:00
let offer = await postOffer ( traders [ 0 ] , { amount : tradeAmount } ) ;
2021-12-14 13:04:02 -05:00
offer = await traders [ 0 ] . getMyOffer ( offer . getId ( ) ) ;
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2022-01-08 05:21:32 -08:00
// wait for offer to be seen
2021-12-16 20:10:40 -05:00
await wait ( TestConfig . walletSyncPeriodMs * 2 ) ;
2021-12-14 13:04:02 -05:00
// trader 1 spends trade funds after initializing trade
let paymentAccount = await createCryptoPaymentAccount ( traders [ 1 ] ) ;
wait ( 3000 ) . then ( async function ( ) {
try {
2022-02-09 01:41:00 -08:00
let traderWallet = await monerojs . connectToWalletRpc ( "http://localhost:" + traders [ 1 ] . getWalletRpcPort ( ) , TestConfig . defaultHavenod . walletUsername , TestConfig . defaultHavenod . accountPassword ) ;
2021-12-14 13:04:02 -05:00
for ( let frozenOutput of await traderWallet . getOutputs ( { isFrozen : true } ) ) await traderWallet . thawOutput ( frozenOutput . getKeyImage ( ) . getHex ( ) ) ;
2022-02-16 10:09:59 -05:00
HavenoUtils . log ( 1 , "Sweeping trade funds" ) ;
2021-12-14 13:04:02 -05:00
await traderWallet . sweepUnlocked ( { address : await traderWallet . getPrimaryAddress ( ) , relay : true } ) ;
} catch ( err ) {
console . log ( "Caught error sweeping funds!" ) ;
console . log ( err ) ;
}
} ) ;
// trader 1 tries to take offer
try {
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Trader 1 taking offer " + offer . getId ( ) ) ;
2021-12-14 13:04:02 -05:00
await traders [ 1 ] . takeOffer ( offer . getId ( ) , paymentAccount . getId ( ) ) ;
throw new Error ( "Should have failed taking offer because taker trade funds spent" )
} catch ( err ) {
2022-02-04 13:16:57 -05:00
assert ( err . message . includes ( "not enough unlocked money" ) , "Unexpected error: " + err . message ) ;
2021-12-14 13:04:02 -05:00
}
// TODO: test that unavailable right after taking (taker will know before maker)
// trader 0's offer remains available
await wait ( 10000 ) ; // give time for trade initialization to fail and offer to become available
offer = await traders [ 0 ] . getMyOffer ( offer . getId ( ) ) ;
if ( offer . getState ( ) !== "AVAILABLE" ) {
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Offer is not yet available, waiting to become available after timeout..." ) ; // TODO (woodser): fail trade on nack during initialization to save a bunch of time
await wait ( TestConfig . tradeInitTimeout - 10000 ) ; // wait remaining time for offer to become available after timeout
2021-12-14 13:04:02 -05:00
offer = await traders [ 0 ] . getMyOffer ( offer . getId ( ) ) ;
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
}
// trader 0 spends trade funds then trader 2 takes offer
wait ( 3000 ) . then ( async function ( ) {
try {
2022-02-09 01:41:00 -08:00
let traderWallet = await monerojs . connectToWalletRpc ( "http://localhost:" + traders [ 0 ] . getWalletRpcPort ( ) , TestConfig . defaultHavenod . walletUsername , TestConfig . defaultHavenod . accountPassword ) ;
2021-12-14 13:04:02 -05:00
for ( let frozenOutput of await traderWallet . getOutputs ( { isFrozen : true } ) ) await traderWallet . thawOutput ( frozenOutput . getKeyImage ( ) . getHex ( ) ) ;
2022-02-16 10:09:59 -05:00
HavenoUtils . log ( 1 , "Sweeping offer funds" ) ;
2021-12-14 13:04:02 -05:00
await traderWallet . sweepUnlocked ( { address : await traderWallet . getPrimaryAddress ( ) , relay : true } ) ;
} catch ( err ) {
console . log ( "Caught error sweeping funds!" ) ;
console . log ( err ) ;
}
} ) ;
// trader 2 tries to take offer
paymentAccount = await createCryptoPaymentAccount ( traders [ 2 ] ) ;
try {
2022-02-16 10:09:59 -05:00
HavenoUtils . log ( 1 , "Trader 2 taking offer" )
2021-12-14 13:04:02 -05:00
await traders [ 2 ] . takeOffer ( offer . getId ( ) , paymentAccount . getId ( ) ) ;
throw new Error ( "Should have failed taking offer because maker trade funds spent" )
} catch ( err ) {
2022-02-04 13:16:57 -05:00
assert ( err . message . includes ( "not enough unlocked money" ) || err . message . includes ( "timeout reached. protocol did not complete" ) , "Unexpected error: " + err . message ) ;
2021-12-14 13:04:02 -05:00
}
// trader 2's balance is unreserved
let trader2Balances = await traders [ 2 ] . getBalances ( ) ;
expect ( BigInt ( trader2Balances . getReservedTradeBalance ( ) ) ) . toEqual ( BigInt ( "0" ) ) ;
expect ( BigInt ( trader2Balances . getUnlockedBalance ( ) ) ) . toBeGreaterThan ( BigInt ( "0" ) ) ;
} catch ( err2 ) {
err = err2 ;
2021-09-17 09:33:58 -04:00
}
2021-12-14 13:04:02 -05:00
// stop traders
2022-02-09 01:41:00 -08:00
for ( let trader of traders ) await releaseHavenoProcess ( trader ) ;
2021-12-14 13:04:02 -05:00
if ( err ) throw err ;
2021-09-17 09:33:58 -04:00
} ) ;
// ------------------------------- HELPERS ------------------------------------
2022-04-05 15:18:36 -04:00
async function initHavenos ( numDaemons : number , config? : any ) {
2022-04-07 16:35:48 -04:00
let traderPromises : Promise < HavenoClient > [ ] = [ ] ;
2022-04-05 15:18:36 -04:00
for ( let i = 0 ; i < numDaemons ; i ++ ) traderPromises . push ( initHaveno ( config ) ) ;
2021-12-14 13:04:02 -05:00
return Promise . all ( traderPromises ) ;
}
2022-04-07 16:35:48 -04:00
async function initHaveno ( config? : any ) : Promise < HavenoClient > {
2022-02-09 01:41:00 -08:00
config = Object . assign ( { } , TestConfig . defaultHavenod , config ) ;
if ( ! config . appName ) config . appName = "haveno-XMR_STAGENET_instance_" + GenUtils . getUUID ( ) ;
2021-12-16 20:10:40 -05:00
2022-02-09 01:41:00 -08:00
// connect to existing server or start new process
let havenod ;
try {
// try to connect to existing server
2022-04-07 16:35:48 -04:00
havenod = new HavenoClient ( config . url , config . apiPassword ) ;
2022-02-09 01:41:00 -08:00
await havenod . getVersion ( ) ;
} catch ( err ) {
// get port for haveno process
let proxyPort = "" ;
if ( config . url ) proxyPort = new URL ( config . url ) . port
else {
for ( let port of Array . from ( TestConfig . proxyPorts . keys ( ) ) ) {
if ( port === "8079" || port === "8080" || port === "8081" ) continue ; // reserved for arbitrator, alice, and bob
if ( ! GenUtils . arrayContains ( HAVENO_PROCESS_PORTS , port ) ) {
HAVENO_PROCESS_PORTS . push ( port ) ;
proxyPort = port ;
break ;
}
2021-12-16 20:10:40 -05:00
}
2021-12-08 06:22:36 -05:00
}
2022-02-09 01:41:00 -08:00
if ( ! proxyPort ) throw new Error ( "No unused test ports available" ) ;
// start haveno process using configured ports if available
let cmd : string [ ] = [
"./haveno-daemon" ,
"--baseCurrencyNetwork" , "XMR_STAGENET" ,
"--useLocalhostForP2P" , "true" ,
"--useDevPrivilegeKeys" , "true" ,
"--nodePort" , TestConfig . proxyPorts . get ( proxyPort ) ! [ 1 ] ,
"--appName" , config . appName ,
"--apiPassword" , "apitest" ,
"--apiPort" , TestConfig . proxyPorts . get ( proxyPort ) ! [ 0 ] ,
"--walletRpcBindPort" , config . walletUrl ? new URL ( config . walletUrl ) . port : "" + await getAvailablePort ( ) , // use configured port if given
"--passwordRequired" , ( config . accountPasswordRequired ? "true" : "false" )
] ;
2022-04-07 16:35:48 -04:00
havenod = await HavenoClient . startProcess ( TestConfig . haveno . path , cmd , "http://localhost:" + proxyPort , config . logProcessOutput ) ;
2022-02-09 01:41:00 -08:00
HAVENO_PROCESSES . push ( havenod ) ;
2021-12-08 06:22:36 -05:00
}
2021-12-16 20:10:40 -05:00
2022-02-09 01:41:00 -08:00
// open account if configured
if ( config . autoLogin ) await initHavenoAccount ( havenod , config . accountPassword ) ;
2021-12-16 20:10:40 -05:00
return havenod ;
2022-02-09 01:41:00 -08:00
async function getAvailablePort ( ) : Promise < number > {
2022-02-11 17:13:56 -06:00
return new Promise ( function ( resolve ) {
2022-02-09 01:41:00 -08:00
let srv = net . createServer ( ) ;
srv . listen ( 0 , function ( ) {
let port = srv . address ( ) . port ;
srv . close ( function ( ) {
resolve ( port ) ;
} ) ;
} ) ;
} ) ;
}
2022-04-04 12:29:35 -07:00
2021-12-08 06:22:36 -05:00
}
/ * *
2022-02-09 01:41:00 -08:00
* Release a Haveno process for reuse and try to shutdown .
2021-12-14 13:04:02 -05:00
* /
2022-04-07 16:35:48 -04:00
async function releaseHavenoProcess ( havenod : HavenoClient ) {
2022-02-09 01:41:00 -08:00
GenUtils . remove ( HAVENO_PROCESSES , havenod ) ;
GenUtils . remove ( HAVENO_PROCESS_PORTS , new URL ( havenod . getUrl ( ) ) . port ) ; // TODO (woodser): standardize to url
try {
await havenod . shutdownServer ( ) ;
} catch ( err ) {
assert . equal ( err . message , OFFLINE_ERR_MSG ) ;
}
2021-12-14 13:04:02 -05:00
}
/ * *
2022-02-09 01:41:00 -08:00
* Create or open an account with the given daemon and password .
2021-12-08 06:22:36 -05:00
* /
2022-04-07 16:35:48 -04:00
async function initHavenoAccount ( havenod : HavenoClient , password : string ) {
2022-02-09 01:41:00 -08:00
if ( await havenod . isAccountOpen ( ) ) return ;
if ( await havenod . accountExists ( ) ) return havenod . openAccount ( password ) ;
await havenod . createAccount ( password ) ;
return ;
2021-12-08 06:22:36 -05:00
}
2021-11-12 10:26:22 -05:00
/ * *
* Open or create funding wallet .
* /
async function initFundingWallet() {
// init client connected to monero-wallet-rpc
2022-02-09 01:41:00 -08:00
fundingWallet = await monerojs . connectToWalletRpc ( TestConfig . fundingWallet . url , TestConfig . fundingWallet . username , TestConfig . fundingWallet . password ) ;
2021-11-12 10:26:22 -05:00
// check if wallet is open
let walletIsOpen = false
try {
await fundingWallet . getPrimaryAddress ( ) ;
walletIsOpen = true ;
} catch ( err ) { }
// open wallet if necessary
if ( ! walletIsOpen ) {
// attempt to open funding wallet
try {
2021-12-16 20:10:40 -05:00
await fundingWallet . openWallet ( { path : TestConfig.fundingWallet.defaultPath , password : TestConfig.fundingWallet.password } ) ;
2021-11-12 10:26:22 -05:00
} catch ( e ) {
if ( ! ( e instanceof monerojs . MoneroRpcError ) ) throw e ;
// -1 returned when wallet does not exist or fails to open e.g. it's already open by another application
if ( e . getCode ( ) === - 1 ) {
// create wallet
2021-12-16 20:10:40 -05:00
await fundingWallet . createWallet ( { path : TestConfig.fundingWallet.defaultPath , password : TestConfig.fundingWallet.password } ) ;
2021-11-12 10:26:22 -05:00
} else {
throw e ;
}
}
}
}
2022-04-06 11:28:56 -04:00
async function startMining() {
try {
await monerod . startMining ( await fundingWallet . getPrimaryAddress ( ) , 3 ) ;
} catch ( err ) {
if ( err . message !== "Already mining" ) throw err ;
}
}
2021-11-12 10:26:22 -05:00
/ * *
* Wait for unlocked balance in wallet or Haveno daemon .
* /
async function waitForUnlockedBalance ( amount : bigint , . . . wallets : any [ ] ) {
// wrap common wallet functionality for tests
class WalletWrapper {
_wallet : any ;
constructor ( wallet : any ) {
this . _wallet = wallet ;
}
async getUnlockedBalance ( ) : Promise < bigint > {
2022-04-07 16:35:48 -04:00
if ( this . _wallet instanceof HavenoClient ) return BigInt ( ( await this . _wallet . getBalances ( ) ) . getUnlockedBalance ( ) ) ;
2021-11-12 10:26:22 -05:00
else return BigInt ( ( await this . _wallet . getUnlockedBalance ( ) ) . toString ( ) ) ;
}
async getLockedBalance ( ) : Promise < bigint > {
2022-04-07 16:35:48 -04:00
if ( this . _wallet instanceof HavenoClient ) return BigInt ( ( await this . _wallet . getBalances ( ) ) . getLockedBalance ( ) ) ;
2021-11-12 10:26:22 -05:00
else return BigInt ( ( await this . _wallet . getBalance ( ) ) . toString ( ) ) - await this . getUnlockedBalance ( ) ;
}
async getDepositAddress ( ) : Promise < string > {
2022-04-07 16:35:48 -04:00
if ( this . _wallet instanceof HavenoClient ) return await this . _wallet . getNewDepositAddress ( ) ;
2021-12-14 13:04:02 -05:00
else return ( await this . _wallet . createSubaddress ( ) ) . getAddress ( ) ;
2021-11-12 10:26:22 -05:00
}
}
// wrap wallets
for ( let i = 0 ; i < wallets . length ; i ++ ) wallets [ i ] = new WalletWrapper ( wallets [ i ] ) ;
// fund wallets with insufficient balance
let miningNeeded = false ;
let fundConfig = new MoneroTxConfig ( ) . setAccountIndex ( 0 ) . setRelay ( true ) ;
for ( let wallet of wallets ) {
let unlockedBalance = await wallet . getUnlockedBalance ( ) ;
if ( unlockedBalance < amount ) miningNeeded = true ;
let depositNeeded : bigint = amount - unlockedBalance - await wallet . getLockedBalance ( ) ;
2022-01-15 15:43:10 -05:00
if ( depositNeeded > BigInt ( "0" ) && wallet . _wallet !== fundingWallet ) {
for ( let i = 0 ; i < 5 ; i ++ ) {
fundConfig . addDestination ( await wallet . getDepositAddress ( ) , depositNeeded * BigInt ( "2" ) ) ; // make several deposits
}
}
2021-11-12 10:26:22 -05:00
}
if ( fundConfig . getDestinations ( ) ) {
2021-12-16 20:10:40 -05:00
await waitForUnlockedBalance ( TestConfig . fundingWallet . minimumFunding , fundingWallet ) ; // TODO (woodser): wait for enough to cover tx amount + fee
2021-11-12 10:26:22 -05:00
try { await fundingWallet . createTx ( fundConfig ) ; }
catch ( err ) { throw new Error ( "Error funding wallets: " + err . message ) ; }
}
// done if all wallets have sufficient unlocked balance
if ( ! miningNeeded ) return ;
// wait for funds to unlock
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 0 , "Mining for unlocked balance of " + amount ) ;
2021-11-12 10:26:22 -05:00
await startMining ( ) ;
2021-12-14 13:04:02 -05:00
let promises : Promise < void > [ ] = [ ] ;
2021-11-12 10:26:22 -05:00
for ( let wallet of wallets ) {
2022-02-11 17:13:56 -06:00
promises . push ( new Promise ( async function ( resolve ) {
2021-11-12 10:26:22 -05:00
let taskLooper : any = new TaskLooper ( async function ( ) {
if ( await wallet . getUnlockedBalance ( ) >= amount ) {
taskLooper . stop ( ) ;
resolve ( ) ;
}
} ) ;
taskLooper . start ( 5000 ) ;
} ) ) ;
}
await Promise . all ( promises ) ;
await monerod . stopMining ( ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 0 , "Funds unlocked, done mining" ) ;
2021-11-12 10:26:22 -05:00
} ;
async function waitForUnlockedTxs ( . . . txHashes : string [ ] ) {
2022-04-06 11:28:56 -04:00
if ( txHashes . length === 0 ) return ;
HavenoUtils . log ( 1 , "Mining to unlock txs" ) ;
2021-11-12 10:26:22 -05:00
await startMining ( ) ;
2022-02-16 10:09:59 -05:00
let promises : Promise < void > [ ] = [ ] ;
2021-11-12 10:26:22 -05:00
for ( let txHash of txHashes ) {
2022-02-16 10:09:59 -05:00
// eslint-disable-next-line no-loop-func
2021-11-12 10:26:22 -05:00
promises . push ( new Promise ( async function ( resolve , reject ) {
2022-02-11 17:13:56 -06:00
let taskLooper = new TaskLooper ( async function ( ) {
2021-11-12 10:26:22 -05:00
let tx = await monerod . getTx ( txHash ) ;
if ( tx . isConfirmed ( ) && tx . getBlock ( ) . getHeight ( ) <= await monerod . getHeight ( ) - 10 ) {
taskLooper . stop ( ) ;
resolve ( ) ;
}
} ) ;
taskLooper . start ( 5000 ) ;
} ) ) ;
}
await Promise . all ( promises ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 1 , "Done mining to unlock txs" ) ;
2021-11-12 10:26:22 -05:00
await monerod . stopMining ( ) ;
}
2022-04-06 11:28:56 -04:00
/ * *
* Indicates if the wallet has an unlocked amount .
*
* @param { MoneroWallet } wallet - wallet to check
* @param { BigInt } amt - amount to check
* @param { number ? } numOutputs - number of outputs of the given amount ( default 1 )
* /
async function hasUnlockedOutputs ( wallet : any , amt : BigInt , numOutputs? : number ) : Promise < boolean > {
if ( numOutputs === undefined ) numOutputs = 1 ;
let availableOutputs = await wallet . getOutputs ( { isSpent : false , isFrozen : false , minAmount : monerojs.BigInteger ( amt . toString ( ) ) , txQuery : { isLocked : false } } ) ;
return availableOutputs . length >= numOutputs ;
}
/ * *
* Fund the given wallets .
*
* @param { MoneroWallet } wallets - monerojs wallets
* @param { BigInt } amt - the amount to fund
* @param { number ? } numOutputs - the number of outputs of the given amount ( default 1 )
* /
async function fundWallets ( wallets : any [ ] , amt : BigInt , numOutputs? : number ) : Promise < void > {
if ( numOutputs === undefined ) numOutputs = 1 ;
// collect destinations
let destinations = [ ] ;
for ( let wallet of wallets ) {
if ( await hasUnlockedOutputs ( wallet , amt , numOutputs ) ) continue ;
for ( let i = 0 ; i < numOutputs ; i ++ ) {
destinations . push ( new MoneroDestination ( ( await wallet . createSubaddress ( ) ) . getAddress ( ) , monerojs . BigInteger ( amt . toString ( ) ) ) ) ;
}
}
// fund destinations
let txConfig = new MoneroTxConfig ( ) . setAccountIndex ( 0 ) . setRelay ( true ) ;
let txHashes : string [ ] = [ ] ;
let sendAmt = BigInteger ( "0" ) ;
for ( let i = 0 ; i < destinations . length ; i ++ ) {
txConfig . addDestination ( destinations [ i ] ) ;
sendAmt = sendAmt . add ( destinations [ i ] . getAmount ( ) ) ;
if ( i === destinations . length - 1 || ( i > 0 && i % 15 === 0 ) ) {
await waitForUnlockedBalance ( toBigInt ( sendAmt ) , fundingWallet ) ;
txHashes . push ( ( await fundingWallet . createTx ( txConfig ) ) . getHash ( ) ) ;
txConfig = new MoneroTxConfig ( ) . setAccountIndex ( 0 ) . setRelay ( true ) ;
sendAmt = BigInteger ( "0" ) ;
}
}
// wait for txs to unlock
if ( txHashes . length > 0 ) {
await waitForUnlockedTxs ( . . . txHashes ) ;
await wait ( 1000 ) ;
for ( let wallet of wallets ) await wallet . sync ( ) ;
2021-11-12 10:26:22 -05:00
}
}
2022-04-06 11:28:56 -04:00
// convert monero-javascript BigInteger to typescript BigInt
function toBigInt ( mjsBigInt : any ) {
return BigInt ( mjsBigInt . toString ( ) )
}
2021-11-12 10:26:22 -05:00
async function wait ( durationMs : number ) {
return new Promise ( function ( resolve ) { setTimeout ( resolve , durationMs ) ; } ) ;
}
2022-01-15 15:43:10 -05:00
function getNotifications ( notifications : NotificationMessage [ ] , notificationType : NotificationMessage.NotificationType ) {
let filteredNotifications : NotificationMessage [ ] = [ ] ;
for ( let notification of notifications ) {
if ( notification . getType ( ) === notificationType ) {
filteredNotifications . push ( notification ) ;
}
}
return filteredNotifications ;
}
2022-02-09 01:41:00 -08:00
function getConnection ( connections : UrlConnection [ ] , url : string ) : UrlConnection | undefined {
for ( let connection of connections ) if ( connection . getUrl ( ) === url ) return connection ;
return undefined ;
}
function testConnection ( connection : UrlConnection , url? : string , onlineStatus? : OnlineStatus , authenticationStatus? : AuthenticationStatus , priority? : number ) {
if ( url ) assert . equal ( connection . getUrl ( ) , url ) ;
assert . equal ( connection . getPassword ( ) , "" ) ; // TODO (woodser): undefined instead of ""?
assert . equal ( connection . getUsername ( ) , "" ) ;
if ( onlineStatus !== undefined ) assert . equal ( connection . getOnlineStatus ( ) , onlineStatus ) ;
if ( authenticationStatus !== undefined ) assert . equal ( connection . getAuthenticationStatus ( ) , authenticationStatus ) ;
if ( priority !== undefined ) assert . equal ( connection . getPriority ( ) , priority ) ;
}
2021-12-30 22:03:00 +02:00
function testTx ( tx : XmrTx , ctx : TxContext ) {
assert ( tx . getHash ( ) ) ;
expect ( BigInt ( tx . getFee ( ) ) ) . toBeLessThan ( TestConfig . maxFee ) ;
if ( tx . getIsConfirmed ( ) ) {
assert ( tx . getTimestamp ( ) > 1000 ) ;
assert ( tx . getHeight ( ) > 0 ) ;
} else {
assert . equal ( tx . getHeight ( ) , 0 ) ;
}
assert ( tx . getOutgoingTransfer ( ) || tx . getIncomingTransfersList ( ) . length ) ; // TODO (woodser): test transfers
for ( let incomingTransfer of tx . getIncomingTransfersList ( ) ) testTransfer ( incomingTransfer , ctx ) ;
if ( tx . getOutgoingTransfer ( ) ) testTransfer ( tx . getOutgoingTransfer ( ) ! , ctx ) ;
2022-02-11 17:13:56 -06:00
if ( ctx . isCreatedTx ) testCreatedTx ( tx ) ;
2021-12-30 22:03:00 +02:00
}
2022-02-11 17:13:56 -06:00
function testCreatedTx ( tx : XmrTx ) {
2021-12-30 22:03:00 +02:00
assert . equal ( tx . getTimestamp ( ) , 0 ) ;
assert . equal ( tx . getIsConfirmed ( ) , false ) ;
assert . equal ( tx . getIsLocked ( ) , true ) ;
assert ( tx . getMetadata ( ) && tx . getMetadata ( ) . length > 0 ) ;
}
2022-02-11 17:13:56 -06:00
function testTransfer ( transfer : XmrIncomingTransfer | XmrOutgoingTransfer , ctx : TxContext ) {
2021-12-30 22:03:00 +02:00
expect ( BigInt ( transfer . getAmount ( ) ) ) . toBeGreaterThanOrEqual ( BigInt ( "0" ) ) ;
assert ( transfer . getAccountIndex ( ) >= 0 ) ;
2022-02-11 17:13:56 -06:00
if ( transfer instanceof XmrIncomingTransfer ) testIncomingTransfer ( transfer ) ;
2021-12-30 22:03:00 +02:00
else testOutgoingTransfer ( transfer , ctx ) ;
}
2022-02-11 17:13:56 -06:00
function testIncomingTransfer ( transfer : XmrIncomingTransfer ) {
2021-12-30 22:03:00 +02:00
assert ( transfer . getAddress ( ) ) ;
assert ( transfer . getSubaddressIndex ( ) >= 0 ) ;
assert ( transfer . getNumSuggestedConfirmations ( ) > 0 ) ;
}
function testOutgoingTransfer ( transfer : XmrOutgoingTransfer , ctx : TxContext ) {
if ( ! ctx . isCreatedTx ) assert ( transfer . getSubaddressIndicesList ( ) . length > 0 ) ;
for ( let subaddressIdx of transfer . getSubaddressIndicesList ( ) ) assert ( subaddressIdx >= 0 ) ;
// test destinations sum to outgoing amount
if ( transfer . getDestinationsList ( ) . length > 0 ) {
let sum = BigInt ( 0 ) ;
for ( let destination of transfer . getDestinationsList ( ) ) {
testDestination ( destination ) ;
expect ( BigInt ( destination . getAmount ( ) ) ) . toBeGreaterThan ( BigInt ( "0" ) ) ;
sum += BigInt ( destination . getAmount ( ) ) ;
}
assert . equal ( sum , BigInt ( transfer . getAmount ( ) ) ) ;
}
}
function testDestination ( destination : XmrDestination ) {
assert ( destination . getAddress ( ) ) ;
expect ( BigInt ( destination . getAmount ( ) ) ) . toBeGreaterThan ( BigInt ( "0" ) ) ;
}
2022-02-16 10:09:59 -05:00
function getRandomAssetCode() {
return TestConfig . assetCodes [ GenUtils . getRandomInt ( 0 , TestConfig . assetCodes . length - 1 ) ] ;
}
2022-04-07 16:35:48 -04:00
async function createPaymentAccount ( trader : HavenoClient , assetCode : string ) : Promise < PaymentAccount > {
2022-02-16 10:09:59 -05:00
return isCrypto ( assetCode ) ? createCryptoPaymentAccount ( trader , assetCode ) : createRevolutPaymentAccount ( trader ) ;
}
function isCrypto ( assetCode : string ) {
return getCryptoAddress ( assetCode ) !== undefined ;
}
function getCryptoAddress ( currencyCode : string ) : string | undefined {
for ( let cryptoAddress of TestConfig . cryptoAddresses ) {
if ( cryptoAddress . currencyCode === currencyCode . toUpperCase ( ) ) return cryptoAddress . address ;
}
}
2022-04-07 16:35:48 -04:00
async function createRevolutPaymentAccount ( trader : HavenoClient ) : Promise < PaymentAccount > {
2022-02-16 10:09:59 -05:00
let accountForm = await trader . getPaymentAccountForm ( 'REVOLUT' ) ;
accountForm . accountName = "Revolut account " + GenUtils . getUUID ( ) ;
accountForm . userName = "user123" ;
return trader . createPaymentAccount ( accountForm ) ;
}
2022-04-07 16:35:48 -04:00
async function createCryptoPaymentAccount ( trader : HavenoClient , currencyCode = "eth" ) : Promise < PaymentAccount > {
2022-02-16 10:09:59 -05:00
for ( let cryptoAddress of TestConfig . cryptoAddresses ) {
if ( cryptoAddress . currencyCode . toLowerCase ( ) !== currencyCode . toLowerCase ( ) ) continue ;
2022-02-11 17:13:56 -06:00
return trader . createCryptoPaymentAccount (
2022-02-16 10:09:59 -05:00
cryptoAddress . currencyCode + " " + cryptoAddress . address . substr ( 0 , 8 ) + "... " + GenUtils . getUUID ( ) ,
cryptoAddress . currencyCode ,
cryptoAddress . address ) ;
2022-02-11 17:13:56 -06:00
}
throw new Error ( "No test config for crypto: " + currencyCode ) ;
2021-12-14 13:04:02 -05:00
}
2022-02-11 17:13:56 -06:00
// TODO: specify counter currency code
2022-04-07 16:35:48 -04:00
async function postOffer ( maker : HavenoClient , config? : any ) {
2022-02-11 17:13:56 -06:00
// assign default options
config = Object . assign ( { } , TestConfig . postOffer , config ) ;
// wait for unlocked balance
if ( config . awaitUnlockedBalance ) await waitForUnlockedBalance ( config . amount * BigInt ( "2" ) , maker ) ;
2021-12-14 13:04:02 -05:00
// create payment account if not given
2022-02-16 10:09:59 -05:00
if ( ! config . paymentAccountId ) config . paymentAccountId = ( await createPaymentAccount ( maker , config . assetCode ) ) . getId ( ) ;
2021-09-14 08:30:22 -04:00
// get unlocked balance before reserving offer
2021-11-19 17:25:49 -05:00
let unlockedBalanceBefore : bigint = BigInt ( ( await maker . getBalances ( ) ) . getUnlockedBalance ( ) ) ;
2021-09-14 08:30:22 -04:00
// post offer
2022-02-11 17:13:56 -06:00
// TODO: re-arrange post offer parameters like this postOffer() or use config interface?
let offer : OfferInfo = await maker . postOffer (
2022-02-16 10:09:59 -05:00
config . assetCode ,
2022-02-11 17:13:56 -06:00
config . direction ,
config . price ,
config . price ? false : true , // TODO: redundant with price field?
config . priceMargin ,
config . amount ,
config . minAmount ,
config . buyerSecurityDeposit ,
config . paymentAccountId ,
config . triggerPrice ) ;
2022-03-07 09:57:00 -08:00
testOffer ( offer , config ) ;
2022-02-16 10:09:59 -05:00
2021-09-14 08:30:22 -04:00
// offer is included in my offers only
2022-02-16 10:09:59 -05:00
if ( ! getOffer ( await maker . getMyOffers ( config . assetCode , config . direction ) , offer . getId ( ) ) ) {
2021-11-19 17:25:49 -05:00
await wait ( 10000 ) ;
2022-02-16 10:09:59 -05:00
if ( ! getOffer ( await maker . getMyOffers ( config . assetCode , config . direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in my offers" ) ;
2021-11-19 17:25:49 -05:00
}
2022-02-16 10:09:59 -05:00
if ( getOffer ( await maker . getOffers ( config . assetCode , config . direction ) , offer . getId ( ) ) ) throw new Error ( "My offer " + offer . getId ( ) + " should not appear in available offers" ) ;
2021-09-14 08:30:22 -04:00
2022-04-06 11:28:56 -04:00
// unlocked balance has decreased
let unlockedBalanceAfter : bigint = BigInt ( ( await maker . getBalances ( ) ) . getUnlockedBalance ( ) ) ;
if ( unlockedBalanceAfter === unlockedBalanceBefore ) throw new Error ( "unlocked balance did not change after posting offer" ) ;
2021-09-17 09:33:58 -04:00
return offer ;
}
2021-09-14 08:30:22 -04:00
function getOffer ( offers : OfferInfo [ ] , id : string ) : OfferInfo | undefined {
return offers . find ( offer = > offer . getId ( ) === id ) ;
}
2021-10-22 13:51:57 -04:00
function testCryptoPaymentAccount ( paymentAccount : PaymentAccount ) {
2021-11-11 13:48:31 -05:00
expect ( paymentAccount . getId ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( paymentAccount . getAccountName ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( paymentAccount . getPaymentAccountPayload ( ) ! . getCryptoCurrencyAccountPayload ( ) ! . getAddress ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( paymentAccount . getSelectedTradeCurrency ( ) ! . getCode ( ) . length ) . toBeGreaterThan ( 0 ) ;
2021-10-22 13:51:57 -04:00
expect ( paymentAccount . getTradeCurrenciesList ( ) . length ) . toEqual ( 1 ) ;
let tradeCurrency = paymentAccount . getTradeCurrenciesList ( ) [ 0 ] ;
2021-11-11 13:48:31 -05:00
expect ( tradeCurrency . getName ( ) . length ) . toBeGreaterThan ( 0 ) ;
2021-10-22 13:51:57 -04:00
expect ( tradeCurrency . getCode ( ) ) . toEqual ( paymentAccount . getSelectedTradeCurrency ( ) ! . getCode ( ) ) ;
2021-09-14 08:27:45 -04:00
}
2022-03-07 09:57:00 -08:00
function testOffer ( offer : OfferInfo , config? : any ) {
2021-11-11 13:48:31 -05:00
expect ( offer . getId ( ) . length ) . toBeGreaterThan ( 0 ) ;
2022-03-07 09:57:00 -08:00
if ( config ) {
expect ( HavenoUtils . centinerosToAtomicUnits ( offer . getAmount ( ) ) ) . toEqual ( config . amount ) ; // TODO (woodser): use atomic units in offer instead of centineros?
expect ( offer . getBuyerSecurityDeposit ( ) / offer . getAmount ( ) ) . toEqual ( config . buyerSecurityDeposit ) ;
expect ( offer . getSellerSecurityDeposit ( ) / offer . getAmount ( ) ) . toEqual ( config . buyerSecurityDeposit ) ; // TODO: use same config.securityDeposit for buyer and seller?
}
2021-09-12 09:39:21 -04:00
// TODO: test rest of offer
2022-03-09 04:43:30 -08:00
}
/ * *
* Tests trade chat functionality . Must be called during an open trade .
* /
2022-04-07 16:35:48 -04:00
async function testTradeChat ( tradeId : string , alice : HavenoClient , bob : HavenoClient ) {
2022-03-09 04:43:30 -08:00
HavenoUtils . log ( 1 , "Testing trade chat" ) ;
// invalid trade should throw error
try {
await alice . getChatMessages ( "invalid" ) ;
throw new Error ( "get chat messages with invalid id should fail" ) ;
} catch ( err ) {
assert . equal ( err . message , "trade with id 'invalid' not found" ) ;
}
// trade chat should be in initial state
let messages = await alice . getChatMessages ( tradeId ) ;
2022-04-04 12:29:35 -07:00
assert ( messages . length === 0 ) ;
2022-03-09 04:43:30 -08:00
messages = await bob . getChatMessages ( tradeId ) ;
2022-04-04 12:29:35 -07:00
assert ( messages . length === 0 ) ;
2022-03-09 04:43:30 -08:00
// add notification handlers and send some messages
let aliceNotifications : NotificationMessage [ ] = [ ] ;
let bobNotifications : NotificationMessage [ ] = [ ] ;
await alice . addNotificationListener ( notification = > { aliceNotifications . push ( notification ) ; } ) ;
await bob . addNotificationListener ( notification = > { bobNotifications . push ( notification ) ; } ) ;
// send simple conversation and verify the list of messages
let aliceMsg = "Hi I'm Alice" ;
await alice . sendChatMessage ( tradeId , aliceMsg ) ;
await wait ( TestConfig . maxTimePeerNoticeMs ) ;
messages = await bob . getChatMessages ( tradeId ) ;
expect ( messages . length ) . toEqual ( 2 ) ;
expect ( messages [ 0 ] . getIsSystemMessage ( ) ) . toEqual ( true ) ; // first message is system
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( aliceMsg ) ;
let bobMsg = "Hello I'm Bob" ;
await bob . sendChatMessage ( tradeId , bobMsg ) ;
await wait ( TestConfig . maxTimePeerNoticeMs ) ;
messages = await alice . getChatMessages ( tradeId ) ;
expect ( messages . length ) . toEqual ( 3 ) ;
expect ( messages [ 0 ] . getIsSystemMessage ( ) ) . toEqual ( true ) ;
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( aliceMsg ) ;
expect ( messages [ 2 ] . getMessage ( ) ) . toEqual ( bobMsg ) ;
// verify notifications
let chatNotifications = getNotifications ( aliceNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( bobMsg ) ;
chatNotifications = getNotifications ( bobNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( aliceMsg ) ;
// additional msgs
let msgs = [ "" , " " , "<script>alert('test');</script>" , "さようなら" ] ;
for ( let msg of msgs ) {
await alice . sendChatMessage ( tradeId , msg ) ;
await wait ( 1000 ) ; // the async operation can result in out of order messages
}
await wait ( TestConfig . maxTimePeerNoticeMs ) ;
messages = await bob . getChatMessages ( tradeId ) ;
let offset = 3 ; // 3 existing messages
expect ( messages . length ) . toEqual ( offset + msgs . length ) ;
expect ( messages [ 0 ] . getIsSystemMessage ( ) ) . toEqual ( true ) ;
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( aliceMsg ) ;
expect ( messages [ 2 ] . getMessage ( ) ) . toEqual ( bobMsg ) ;
for ( var i = 0 ; i < msgs . length ; i ++ ) {
2022-04-04 12:29:35 -07:00
expect ( messages [ i + offset ] . getMessage ( ) ) . toEqual ( msgs [ i ] ) ;
2022-03-09 04:43:30 -08:00
}
chatNotifications = getNotifications ( bobNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
offset = 1 ; // 1 existing notification
expect ( chatNotifications . length ) . toBe ( offset + msgs . length ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( aliceMsg ) ;
2022-04-06 11:28:56 -04:00
for ( let i = 0 ; i < msgs . length ; i ++ ) {
2022-03-09 04:43:30 -08:00
expect ( chatNotifications [ i + offset ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( msgs [ i ] ) ;
}
2022-04-04 12:29:35 -07:00
}
function testMoneroNodeSettings ( settingsBefore : MoneroNodeSettings , settingsAfter : MoneroNodeSettings ) {
expect ( settingsBefore . getBlockchainPath ( ) ) . toEqual ( settingsAfter . getBlockchainPath ( ) ) ;
expect ( settingsBefore . getBootstrapUrl ( ) ) . toEqual ( settingsAfter . getBootstrapUrl ( ) ) ;
expect ( settingsBefore . getStartupFlagsList ( ) ) . toEqual ( settingsAfter . getStartupFlagsList ( ) ) ;
2022-03-07 09:57:00 -08:00
}