2024-03-24 07:47:07 -04:00
/ *
* Copyright Haveno
*
* Licensed under the Apache License , Version 2.0 ( the "License" ) ;
* you may not use this file except in compliance with the License .
* You may obtain a copy of the License at
*
* http : //www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing , software
* distributed under the License is distributed on an "AS IS" BASIS ,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
* See the License for the specific language governing permissions and
* limitations under the License .
* /
2021-11-12 10:26:22 -05:00
// --------------------------------- IMPORTS ----------------------------------
2022-02-11 17:13:56 -06:00
2023-10-02 08:16:54 -04:00
// haveno imports
2023-11-09 17:17:14 -05:00
import {
HavenoClient ,
HavenoError ,
HavenoUtils ,
OfferDirection ,
MarketPriceInfo ,
NotificationMessage ,
OfferInfo ,
TradeInfo ,
UrlConnection ,
XmrBalanceInfo ,
Attachment ,
DisputeResult ,
PaymentMethod ,
2024-08-29 11:32:14 -04:00
PaymentAccount ,
2023-11-09 17:17:14 -05:00
PaymentAccountForm ,
PaymentAccountFormField ,
2024-08-29 11:32:14 -04:00
PaymentAccountPayload ,
2023-11-09 17:17:14 -05:00
XmrDestination ,
2023-11-25 14:48:58 -05:00
XmrNodeSettings ,
2023-11-09 17:17:14 -05:00
XmrTx ,
XmrIncomingTransfer ,
XmrOutgoingTransfer ,
} from "./index" ;
2022-02-09 01:41:00 -08:00
import AuthenticationStatus = UrlConnection . AuthenticationStatus ;
import OnlineStatus = UrlConnection . OnlineStatus ;
2021-09-12 09:39:21 -04:00
2022-07-07 09:11:50 -04:00
// other imports
2022-05-01 13:30:11 -04:00
import fs from "fs" ;
import path from "path" ;
import net from "net" ;
import assert from "assert" ;
import console from "console" ; // import console because jest swallows messages in real time
2024-01-27 17:36:42 -05:00
import moneroTs from "monero-ts" ;
2022-07-07 09:11:50 -04:00
import * as os from 'os' ;
2021-09-12 09:39:21 -04:00
2021-12-16 20:10:40 -05:00
// ------------------------------ TEST CONFIG ---------------------------------
2022-07-07 09:11:50 -04:00
enum BaseCurrencyNetwork {
XMR_MAINNET = "XMR_MAINNET" ,
XMR_STAGENET = "XMR_STAGENET" ,
XMR_LOCAL = "XMR_LOCAL"
}
2022-10-01 07:47:46 -04:00
// clients
const startupHavenods : HavenoClient [ ] = [ ] ;
let arbitrator : HavenoClient ;
let user1 : HavenoClient ;
let user2 : HavenoClient ;
2023-10-02 08:16:54 -04:00
let monerod : moneroTs.MoneroDaemon ;
let fundingWallet : moneroTs.MoneroWalletRpc ;
let user1Wallet : moneroTs.MoneroWalletRpc ;
let user2Wallet : moneroTs.MoneroWalletRpc ;
2022-10-01 07:47:46 -04:00
2023-11-09 17:17:14 -05:00
enum TradeRole {
MAKER = "MAKER" ,
TAKER = "TAKER" ,
}
enum SaleRole {
BUYER = "BUYER" ,
SELLER = "SELLER"
}
enum DisputeContext {
NONE = "NONE" ,
OPEN_AFTER_DEPOSITS_UNLOCK = "OPEN_AFTER_DEPOSITS_UNLOCK" ,
OPEN_AFTER_PAYMENT_SENT = "OPEN_AFTER_PAYMENT_SENT"
}
2023-10-28 10:20:56 -04:00
/ * *
* Test context for a single peer in a trade .
* /
class PeerContext {
2024-08-29 11:32:14 -04:00
havenod? : HavenoClient ;
wallet? : moneroTs.MoneroWallet ;
trade? : TradeInfo ;
2023-10-28 10:20:56 -04:00
// context to test balances after trade
2024-08-29 11:32:14 -04:00
balancesBeforeOffer? : XmrBalanceInfo ;
splitOutputTxFee? : bigint ;
balancesBeforeTake? : XmrBalanceInfo ;
balancesAfterTake? : XmrBalanceInfo ;
balancesBeforePayout? : XmrBalanceInfo ;
balancesAfterPayout? : XmrBalanceInfo ;
tradeFee? : bigint ;
depositTx? : moneroTs.MoneroTx ;
depositTxFee? : bigint ;
securityDepositActual? : bigint ;
payoutTxFee? : bigint ;
payoutAmount? : bigint ;
2023-10-28 10:20:56 -04:00
constructor ( ctx? : Partial < PeerContext > ) {
Object . assign ( this , ctx ) ;
}
}
/ * *
* Default trade configuration .
* /
const defaultTradeConfig : Partial < TradeContext > = {
arbitrator : new PeerContext ( ) ,
maker : new PeerContext ( ) ,
taker : new PeerContext ( ) ,
makeOffer : true ,
takeOffer : true ,
awaitFundsToMakeOffer : true ,
2023-11-09 17:17:14 -05:00
direction : OfferDirection.BUY , // buy or sell xmr
2024-04-07 08:13:09 -04:00
offerAmount : 193312996088n ,
2023-10-28 10:20:56 -04:00
offerMinAmount : undefined ,
assetCode : "usd" , // counter asset to trade
makerPaymentAccountId : undefined ,
2023-10-30 17:11:33 -04:00
securityDepositPct : 0.15 ,
2023-10-28 10:20:56 -04:00
price : undefined , // use market price if undefined
triggerPrice : undefined ,
awaitFundsToTakeOffer : true ,
offerId : undefined ,
takerPaymentAccountId : undefined ,
buyerSendsPayment : true ,
sellerReceivesPayment : true ,
resolveDispute : true , // resolve dispute after opening
disputeWinner : DisputeResult.Winner.SELLER ,
disputeReason : DisputeResult.Reason.PEER_WAS_LATE ,
disputeSummary : "Seller is winner" ,
walletSyncPeriodMs : 5000 ,
maxTimePeerNoticeMs : 5000 ,
2024-04-26 15:00:01 -04:00
testChatMessages : true ,
2024-05-05 15:11:52 -04:00
stopOnFailure : false , // TODO: setting to true can cause error: Http response at 400 or 500 level, http status code: 503
2023-10-28 10:20:56 -04:00
testPayoutConfirmed : true ,
2024-04-08 07:27:03 -04:00
testPayoutUnlocked : false ,
maxConcurrency : getMaxConcurrency ( )
2023-10-28 10:20:56 -04:00
}
/ * *
* Configuration and context for a single trade .
* /
class TradeContext {
// trade peers
2024-08-29 11:32:14 -04:00
arbitrator ! : Partial < PeerContext > ;
maker ! : Partial < PeerContext > ;
taker ! : Partial < PeerContext > ;
2023-10-28 10:20:56 -04:00
// trade flow
concurrentTrades? : boolean ; // testing trades at same time
makeOffer? : boolean ;
takeOffer? : boolean ;
buyerOfflineAfterTake? : boolean ;
sellerOfflineAfterTake? : boolean ;
buyerOfflineAfterPaymentSent? : boolean
buyerOfflineAfterDisputeOpened? : boolean ;
sellerOfflineAfterDisputeOpened? : boolean ;
sellerDisputeContext? : DisputeContext ;
buyerDisputeContext? : DisputeContext ;
buyerSendsPayment? : boolean ;
sellerReceivesPayment? : boolean
// make offer
awaitFundsToMakeOffer? : boolean
2023-11-09 17:17:14 -05:00
direction? : OfferDirection ;
2023-10-28 10:20:56 -04:00
assetCode? : string ;
offerAmount? : bigint ; // offer amount or max
offerMinAmount? : bigint ;
tradeAmount? : bigint ; // trade amount within offer range
makerPaymentAccountId? : string ;
2023-10-30 17:11:33 -04:00
securityDepositPct? : number ;
2023-10-28 10:20:56 -04:00
price? : number ;
priceMargin? : number ;
triggerPrice? : number ;
reserveExactAmount? : boolean ;
// take offer
awaitFundsToTakeOffer? : boolean ;
offerId? : string ;
takerPaymentAccountId? : string ;
testTraderChat? : boolean ;
// resolve dispute
resolveDispute? : boolean
disputeOpener? : SaleRole ;
disputeWinner? : DisputeResult.Winner ;
disputeReason? : DisputeResult.Reason ;
disputeSummary? : string ;
disputeWinnerAmount? : bigint ;
// other context
offer? : OfferInfo ;
index? : number ;
isOfferTaken? : boolean ;
isPaymentSent? : boolean ;
isPaymentReceived? : boolean ;
phase? : string ;
payoutState? : string [ ] ;
disputeState? : string ;
isCompleted? : boolean ;
isPayoutPublished? : boolean ; // TODO: test isDepositsPublished; etc
isPayoutConfirmed? : boolean ;
isPayoutUnlocked? : boolean
buyerOpenedDispute? : boolean ;
sellerOpenedDispute? : boolean ;
2024-08-29 11:32:14 -04:00
walletSyncPeriodMs ! : number ;
maxTimePeerNoticeMs ! : number ;
testChatMessages ! : boolean ;
2023-10-28 10:20:56 -04:00
stopOnFailure? : boolean ;
buyerAppName? : string ;
sellerAppName? : string ;
usedPorts? : string [ ] ;
testPayoutConfirmed? : boolean ;
testPayoutUnlocked? : boolean ;
payoutTxId? : string
testBalanceChangeEndToEnd? : boolean ;
2024-08-29 11:32:14 -04:00
isStopped ! : boolean ;
maxConcurrency ! : number ;
2023-10-28 10:20:56 -04:00
constructor ( ctx? : Partial < TradeContext > ) {
Object . assign ( this , ctx ) ;
if ( this . arbitrator ) this . arbitrator = new PeerContext ( this . arbitrator ) ;
if ( this . maker ) this . maker = new PeerContext ( this . maker ) ;
if ( this . taker ) this . taker = new PeerContext ( this . taker ) ;
}
getMaker ( ) : PeerContext {
return this . maker as PeerContext ;
}
getTaker ( ) : PeerContext {
return this . taker as PeerContext ;
}
getBuyer ( ) : PeerContext {
2023-11-09 17:17:14 -05:00
return ( this . direction === OfferDirection . BUY ? this . maker : this.taker ) as PeerContext ;
2023-10-28 10:20:56 -04:00
}
getSeller ( ) : PeerContext {
2023-11-09 17:17:14 -05:00
return ( this . direction === OfferDirection . BUY ? this . taker : this.maker ) as PeerContext ;
2023-10-28 10:20:56 -04:00
}
isBuyerMaker ( ) : boolean {
2023-11-09 17:17:14 -05:00
return this . direction === OfferDirection . BUY ;
2023-10-28 10:20:56 -04:00
}
getDisputeOpener ( ) : PeerContext | undefined {
if ( this . disputeOpener === undefined ) return undefined ;
return this . disputeOpener === SaleRole . BUYER ? this . getBuyer ( ) : this . getSeller ( ) ;
}
getDisputePeer ( ) : PeerContext | undefined {
if ( this . disputeOpener === undefined ) return undefined ;
return this . disputeOpener === SaleRole . BUYER ? this . getSeller ( ) : this . getBuyer ( ) ;
}
getDisputeWinner ( ) : PeerContext | undefined {
if ( this . disputeWinner === undefined ) return undefined ;
return this . disputeWinner === DisputeResult . Winner . BUYER ? this . getBuyer ( ) : this . getSeller ( ) ;
}
getDisputeLoser ( ) : PeerContext | undefined {
if ( this . disputeWinner === undefined ) return undefined ;
return this . disputeWinner === DisputeResult . Winner . BUYER ? this . getSeller ( ) : this . getBuyer ( ) ;
}
2023-12-07 18:05:15 -05:00
isOfflineFlow() {
return this . buyerOfflineAfterDisputeOpened || this . sellerOfflineAfterDisputeOpened || this . buyerOfflineAfterPaymentSent || this . buyerOfflineAfterTake || this . sellerOfflineAfterTake ;
}
2023-12-15 09:24:23 -05:00
getPhase() {
return this . isPaymentReceived ? "PAYMENT_RECEIVED" : this . isPaymentSent ? "PAYMENT_SENT" : "DEPOSITS_UNLOCKED" ;
}
2023-10-28 10:20:56 -04:00
static init ( ctxP : Partial < TradeContext > | undefined ) : TradeContext {
let ctx = ctxP instanceof TradeContext ? ctxP : new TradeContext ( ctxP ) ;
if ( ! ctx . offerAmount && ctx . tradeAmount ) ctx . offerAmount = ctx . tradeAmount ;
if ( ! ctx . offerMinAmount && ctx . offerAmount ) ctx . offerMinAmount = ctx . offerAmount ;
Object . assign ( ctx , new TradeContext ( TestConfig . trade ) , Object . assign ( { } , ctx ) ) ;
return ctx ;
}
async toSummary ( ) : Promise < string > {
let str : string = "" ;
2023-11-09 17:17:14 -05:00
str += "Type: Maker/" + ( this . direction === OfferDirection . BUY ? "Buyer" : "Seller" ) + ", Taker/" + ( this . direction === OfferDirection . BUY ? "Seller" : "Buyer" ) ;
2023-10-28 10:20:56 -04:00
str += "\nOffer id: " + this . offerId ;
if ( this . maker . havenod ) str += "\nMaker uri: " + this . maker ? . havenod ? . getUrl ( ) ;
if ( this . taker . havenod ) str += "\nTaker uri: " + this . taker ? . havenod ? . getUrl ( ) ;
str += "\nAsset code: " + this . assetCode ? . toUpperCase ( ) ;
str += "\nMaker payment account id: " + this . makerPaymentAccountId ;
str += "\nTaker payment account id: " + this . takerPaymentAccountId ;
str += "\nTrade amount: " + this . tradeAmount ;
str += "\nMin amount: " + this . offerMinAmount ;
str += "\nMax amount: " + this . offerAmount ;
2023-10-30 17:11:33 -04:00
str += "\nSecurity deposit percent: " + this . securityDepositPct ;
2023-10-28 10:20:56 -04:00
str += "\nMaker balance before offer: " + this . maker . balancesBeforeOffer ? . getBalance ( ) ;
str += "\nMaker split output tx fee: " + this . maker . splitOutputTxFee ;
2024-04-07 08:13:09 -04:00
if ( this . offer ) {
str += "\nMaker fee percent: " + this . offer ! . getMakerFeePct ( ) ;
str += "\nTaker fee percent: " + this . offer ! . getTakerFeePct ( ) ;
}
2023-10-28 10:20:56 -04:00
if ( this . arbitrator && this . arbitrator ! . trade ) {
2024-04-07 08:13:09 -04:00
str += "\nMaker trade fee: " + this . arbitrator ? . trade ? . getMakerFee ( ) ;
2023-10-28 10:20:56 -04:00
str += "\nMaker deposit tx id: " + this . arbitrator ! . trade ! . getMakerDepositTxId ( ) ;
if ( this . arbitrator ! . trade ! . getMakerDepositTxId ( ) ) {
let tx = await monerod . getTx ( this . arbitrator ! . trade ! . getMakerDepositTxId ( ) ) ;
str += "\nMaker deposit tx fee: " + ( tx ? tx ? . getFee ( ) : undefined ) ;
}
2023-11-09 17:17:14 -05:00
str += "\nMaker security deposit received: " + ( this . direction == OfferDirection . BUY ? this . arbitrator ! . trade ! . getBuyerSecurityDeposit ( ) : this . arbitrator ! . trade ! . getSellerSecurityDeposit ( ) ) ;
2023-10-28 10:20:56 -04:00
}
str += "\nTaker balance before offer: " + this . taker . balancesBeforeOffer ? . getBalance ( ) ;
if ( this . arbitrator && this . arbitrator ! . trade ) {
str += "\nTaker trade fee: " + this . arbitrator ? . trade ? . getTakerFee ( ) ;
str += "\nTaker deposit tx id: " + this . arbitrator ! . trade ! . getTakerDepositTxId ( ) ;
if ( this . arbitrator ! . trade ! . getTakerDepositTxId ( ) ) {
let tx = await monerod . getTx ( this . arbitrator ! . trade ! . getTakerDepositTxId ( ) ) ;
str += "\nTaker deposit tx fee: " + ( tx ? tx ? . getFee ( ) : undefined ) ;
}
2023-11-09 17:17:14 -05:00
str += "\nTaker security deposit received: " + ( this . direction == OfferDirection . BUY ? this . arbitrator ! . trade ! . getSellerSecurityDeposit ( ) : this . arbitrator ! . trade ! . getBuyerSecurityDeposit ( ) ) ;
2023-10-28 10:20:56 -04:00
if ( this . disputeWinner ) str += "\nDispute winner: " + ( this . disputeWinner == DisputeResult . Winner . BUYER ? "Buyer" : "Seller" ) ;
str += "\nPayout tx id: " + this . payoutTxId ;
if ( this . payoutTxId ) {
2024-08-16 12:01:41 -04:00
str += "\nPayout fee: " + ( await monerod . getTx ( this . payoutTxId ! ) ) ! . getFee ( ) ! ;
2023-10-28 10:20:56 -04:00
if ( this . getBuyer ( ) . havenod ) str += "\nBuyer payout: " + ( await this . getBuyer ( ) . havenod ! . getXmrTx ( this . payoutTxId ! ) ) ? . getIncomingTransfersList ( ) [ 0 ] . getAmount ( ) ! ;
2024-08-06 05:10:08 -04:00
if ( this . getSeller ( ) . havenod ) str += "\nSeller payout: " + ( await this . getSeller ( ) . havenod ! . getXmrTx ( this . payoutTxId ! ) ) ? . getIncomingTransfersList ( ) [ 0 ] . getAmount ( ) ! ;
2023-10-28 10:20:56 -04:00
}
}
str += "\nOffer json: " + JSON . stringify ( this . offer ? . toObject ( ) ) ;
return str ;
}
}
/ * *
* Default test configuration .
* /
2021-12-16 20:10:40 -05:00
const TestConfig = {
2022-07-15 10:09:56 -04:00
logLevel : 2 ,
2022-07-07 09:11:50 -04:00
baseCurrencyNetwork : getBaseCurrencyNetwork ( ) ,
2023-10-02 08:16:54 -04:00
networkType : getBaseCurrencyNetwork ( ) == BaseCurrencyNetwork . XMR_MAINNET ? moneroTs.MoneroNetworkType.MAINNET : getBaseCurrencyNetwork ( ) == BaseCurrencyNetwork . XMR_LOCAL ? moneroTs.MoneroNetworkType.TESTNET : moneroTs.MoneroNetworkType.STAGENET ,
2022-01-24 19:37:18 +01:00
moneroBinsDir : "../haveno/.localnet" ,
2022-04-04 12:29:35 -07:00
testDataDir : "./testdata" ,
2021-12-16 20:10:40 -05:00
haveno : {
path : "../haveno" ,
2024-09-04 08:40:40 -04:00
version : "1.0.11"
2021-12-16 20:10:40 -05:00
} ,
monerod : {
2024-01-17 11:22:12 -05:00
url : "http://localhost:" + getNetworkStartPort ( ) + "8081" , // 18081, 28081, 38081 for mainnet, testnet, and stagenet, respectively
2022-07-07 09:11:50 -04:00
username : "" ,
password : ""
2022-01-24 19:37:18 +01:00
} ,
2024-01-17 11:22:12 -05:00
monerod3 : { // corresponds to monerod3-local in Makefile
2022-02-09 01:41:00 -08:00
url : "http://localhost:58081" ,
2021-12-16 20:10:40 -05:00
username : "superuser" ,
2022-07-07 09:11:50 -04:00
password : "abctesting123" ,
p2pBindPort : "58080" ,
rpcBindPort : "58081" ,
zmqRpcBindPort : "58082"
2021-12-16 20:10:40 -05:00
} ,
fundingWallet : {
2022-07-07 09:11:50 -04:00
url : "http://localhost:" + getNetworkStartPort ( ) + "8084" , // 18084, 28084, 38084 for mainnet, testnet, stagenet respectively
2021-12-16 20:10:40 -05:00
username : "rpc_user" ,
password : "abc123" ,
2022-07-30 17:11:40 -04:00
walletPassword : "abc123" ,
2022-07-07 09:11:50 -04:00
defaultPath : "funding_wallet-" + getBaseCurrencyNetwork ( ) ,
2023-12-27 16:58:30 -05:00
minimumFunding : 5000000000000n ,
2023-07-25 09:14:04 -04:00
seed : "origin hickory pavements tudor sizes hornet tether segments sack technical elbow unsafe legion nitrogen adapt yearbook idols fuzzy pitched goes tusks elbow erase fossil erase" ,
2023-05-25 15:14:01 -04:00
primaryAddress : "9xSyMy1r9h3BVjMrF3CTqQCQy36yCfkpn7uVfMyTUbez3hhumqBUqGUNNALjcd7f1HJBRdeH82bCC3veFHW7z3xm28gug4d" ,
2022-08-06 18:38:31 -04:00
restoreHeight : 150
2021-12-16 20:10:40 -05:00
} ,
2022-02-09 01:41:00 -08:00
defaultHavenod : {
2022-07-15 10:09:56 -04:00
logProcessOutput : true , // log output for processes started by tests (except arbitrator, user1, and user2 which are configured separately)
2023-01-18 10:11:47 -05:00
logLevel : "info" ,
2022-02-09 01:41:00 -08:00
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 : [ {
2022-11-04 15:57:42 -04:00
appName : "haveno-" + getBaseCurrencyNetwork ( ) + "_arbitrator" , // arbritrator
2022-07-07 09:11:50 -04:00
logProcessOutput : true ,
2022-12-03 13:03:44 +00:00
port : "8079" ,
2022-02-09 01:41:00 -08:00
accountPasswordRequired : false ,
accountPassword : "abctesting123" ,
} , {
2022-11-04 15:57:42 -04:00
appName : "haveno-" + getBaseCurrencyNetwork ( ) + "_user1" , // user1
2022-07-07 09:11:50 -04:00
logProcessOutput : true ,
2022-12-03 13:03:44 +00:00
port : "8080" ,
2022-02-09 01:41:00 -08:00
accountPasswordRequired : false ,
accountPassword : "abctesting456" ,
walletUrl : "http://127.0.0.1:38091" ,
} , {
2022-11-04 15:57:42 -04:00
appName : "haveno-" + getBaseCurrencyNetwork ( ) + "_user2" , // user2
2022-07-07 09:11:50 -04:00
logProcessOutput : true ,
2022-12-03 13:03:44 +00:00
port : "8081" ,
2022-02-09 01:41:00 -08:00
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
}
] ,
2023-09-09 10:25:52 -04:00
maxFee : HavenoUtils.xmrToAtomicUnits ( 0.5 ) , // local testnet fees can be relatively high
2023-10-02 08:16:54 -04:00
minSecurityDeposit : moneroTs.MoneroUtils.xmrToAtomicUnits ( 0.1 ) ,
2023-09-09 10:25:52 -04:00
maxAdjustmentPct : 0.2 ,
2023-02-27 12:57:06 -05:00
daemonPollPeriodMs : 5000 ,
2022-04-06 11:28:56 -04:00
maxWalletStartupMs : 10000 , // TODO (woodser): make shorter by switching to jni
2022-07-07 09:11:50 -04:00
maxCpuPct : 0.25 ,
2023-10-10 07:02:36 -04:00
paymentMethods : Object.keys ( PaymentAccountForm . FormId ) , // all supported payment methods
2022-08-17 13:26:24 -04:00
assetCodes : [ "USD" , "GBP" , "EUR" , "ETH" , "BTC" , "BCH" , "LTC" ] , // primary asset codes
2023-10-28 13:35:12 -04:00
fixedPriceAssetCodes : [ "XAG" , "XAU" , "XGB" ] ,
2022-02-16 10:09:59 -05:00
cryptoAddresses : [ {
2021-12-16 20:10:40 -05:00
currencyCode : "ETH" ,
address : "0xdBdAb835Acd6fC84cF5F9aDD3c0B5a1E25fbd99f"
} , {
currencyCode : "BTC" ,
2022-11-15 12:44:26 +01:00
address : "1G457efxTyci67msm2dSqyhFzxPYFWaghe"
2022-03-06 10:49:26 -05:00
} , {
currencyCode : "BCH" ,
2022-11-24 12:36:51 +00:00
address : "qz54ydhwzn25wzf8pge5s26udvtx33yhyq3lnv6vq6"
2022-02-16 11:59:31 -05:00
} , {
currencyCode : "LTC" ,
address : "LXUTUN5mTPc2LsS7cEjkyjTRcfYyJGoUuQ"
2021-12-16 20:10:40 -05:00
}
] ,
2022-12-03 13:03:44 +00:00
ports : new Map < string , string [ ] > ( [ // map http ports to havenod api and p2p ports
[ "8079" , [ "9998" , "4444" ] ] , // arbitrator
[ "8080" , [ "9999" , "5555" ] ] , // user1
[ "8081" , [ "10000" , "6666" ] ] , // user2
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-08-12 08:28:58 -04:00
arbitratorPrivKeys : {
XMR_LOCAL : [ "6ac43ea1df2a290c1c8391736aa42e4339c5cb4f110ff0257a13b63211977b7a" , "d96c4e7be030564cfa64a4040060574a8e92a79f574104ab8bb0c1166db28047" , "6d5c86cbc5fc7ce3c97b06969661eae5c018cb2923856cc51341d182a45d1e9d" ] ,
XMR_STAGENET : [ "1aa111f817b7fdaaec1c8d5281a1837cc71c336db09b87cf23344a0a4e3bb2cb" , "6b5a404eb5ff7154f2357126c84c3becfe2e7c59ca3844954ce9476bec2a6228" , "fd4ef301a2e4faa3c77bc26393919895fa29b0908f2bbd51f6f6de3e46fb7a6e" ] ,
XMR_MAINNET : [ ]
} ,
2024-04-30 15:35:12 -04:00
tradeStepTimeoutMs : getBaseCurrencyNetwork ( ) === BaseCurrencyNetwork . XMR_LOCAL ? 60000 : 180000 ,
2023-02-27 16:04:44 -05:00
testTimeout : getBaseCurrencyNetwork ( ) === BaseCurrencyNetwork . XMR_LOCAL ? 2400000 : 5400000 , // timeout in ms for each test to complete (40 minutes for private network, 90 minutes for public network)
2023-10-28 10:20:56 -04:00
trade : new TradeContext ( defaultTradeConfig )
2021-12-16 20:10:40 -05:00
} ;
2021-11-12 10:26:22 -05:00
2022-12-03 13:03:44 +00:00
interface HavenodContext {
logProcessOutput? : boolean ,
2023-01-18 10:11:47 -05:00
logLevel? : string ,
2022-12-03 13:03:44 +00:00
apiPassword? : string ,
walletUsername? : string ,
walletDefaultPassword? : string ,
accountPasswordRequired? : boolean ,
accountPassword? : string ,
autoLogin? : boolean ,
appName? : string ,
port? : string ,
excludePorts? : string [ ] ,
walletUrl? : string
}
2022-10-01 07:47:46 -04:00
interface TxContext {
isCreatedTx : boolean ;
}
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 [ ] = [ ] ;
2022-11-04 15:57:42 -04:00
const HAVENO_WALLETS : Map < HavenoClient , any > = new Map < HavenoClient , any > ( ) ;
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" ;
2024-04-27 10:33:46 -04:00
function getMaxConcurrency() {
return isGitHubActions ( ) ? 4 : 20 ;
}
function isGitHubActions() {
return process . env . GITHUB_ACTIONS === 'true' ;
}
2021-12-16 20:10:40 -05:00
// -------------------------- BEFORE / AFTER TESTS ----------------------------
2021-11-12 10:26:22 -05:00
2022-07-07 09:11:50 -04:00
jest . setTimeout ( TestConfig . testTimeout ) ;
2022-02-09 01:41:00 -08:00
2021-09-17 09:33:58 -04:00
beforeAll ( async ( ) = > {
2023-02-26 10:36:14 -05:00
try {
2022-12-13 08:44:20 +00:00
2023-02-26 10:36:14 -05:00
// set log level for tests
HavenoUtils . setLogLevel ( TestConfig . logLevel ) ;
// initialize funding wallet
2024-06-20 10:47:04 -04:00
HavenoUtils . log ( 0 , "Initializing funding wallet" ) ;
2023-02-26 10:36:14 -05:00
await initFundingWallet ( ) ;
HavenoUtils . log ( 0 , "Funding wallet balance: " + await fundingWallet . getBalance ( ) ) ;
HavenoUtils . log ( 0 , "Funding wallet unlocked balance: " + await fundingWallet . getUnlockedBalance ( ) ) ;
const subaddress = await fundingWallet . createSubaddress ( 0 ) ;
HavenoUtils . log ( 0 , "Funding wallet height: " + await fundingWallet . getHeight ( ) ) ;
2023-07-25 09:14:04 -04:00
HavenoUtils . log ( 0 , "Funding wallet seed: " + await fundingWallet . getSeed ( ) ) ;
2023-02-26 10:36:14 -05:00
HavenoUtils . log ( 0 , "Funding wallet primary address: " + await fundingWallet . getPrimaryAddress ( ) ) ;
HavenoUtils . log ( 0 , "Funding wallet new subaddress: " + subaddress . getAddress ( ) ) ;
// initialize monerod
2023-04-07 14:52:24 -04:00
try {
2023-10-02 08:16:54 -04:00
monerod = await moneroTs . connectToDaemonRpc ( TestConfig . monerod . url , TestConfig . monerod . username , TestConfig . monerod . password ) ;
2023-04-07 14:52:24 -04:00
await mineToHeight ( 160 ) ; // initialize blockchain to latest block type
2024-08-29 11:32:14 -04:00
} catch ( err : any ) {
2023-04-07 14:52:24 -04:00
HavenoUtils . log ( 0 , "Error initializing internal monerod: " + err . message ) ; // allowed in order to test starting and stopping local node
}
2023-02-26 10:36:14 -05:00
// start configured haveno daemons
const promises : Promise < HavenoClient > [ ] = [ ] ;
let err ;
for ( const config of TestConfig . startupHavenods ) promises . push ( initHaveno ( config ) ) ;
for ( const settledPromise of await Promise . allSettled ( promises ) ) {
if ( settledPromise . status === "fulfilled" ) startupHavenods . push ( ( settledPromise as PromiseFulfilledResult < HavenoClient > ) . value ) ;
else if ( ! err ) err = new Error ( ( settledPromise as PromiseRejectedResult ) . reason ) ;
}
if ( err ) throw err ;
2022-12-13 08:44:20 +00:00
2023-02-26 10:36:14 -05:00
// assign arbitrator, user1, user2
arbitrator = startupHavenods [ 0 ] ;
user1 = startupHavenods [ 1 ] ;
user2 = startupHavenods [ 2 ] ;
2023-10-28 10:20:56 -04:00
TestConfig . trade . arbitrator . havenod = arbitrator ;
TestConfig . trade . maker . havenod = user1 ;
TestConfig . trade . taker . havenod = user2 ;
2022-01-08 05:21:32 -08:00
2023-02-26 10:36:14 -05:00
// connect client wallets
2023-10-02 08:16:54 -04:00
user1Wallet = await moneroTs . connectToWalletRpc ( TestConfig . startupHavenods [ 1 ] . walletUrl ! , TestConfig . defaultHavenod . walletUsername , TestConfig . startupHavenods [ 1 ] . accountPasswordRequired ? TestConfig . startupHavenods [ 1 ] . accountPassword : TestConfig.defaultHavenod.walletDefaultPassword ) ;
user2Wallet = await moneroTs . connectToWalletRpc ( TestConfig . startupHavenods [ 2 ] . walletUrl ! , TestConfig . defaultHavenod . walletUsername , TestConfig . startupHavenods [ 2 ] . accountPasswordRequired ? TestConfig . startupHavenods [ 2 ] . accountPassword : TestConfig.defaultHavenod.walletDefaultPassword ) ;
2022-12-13 08:44:20 +00:00
2023-02-26 10:36:14 -05:00
// register arbitrator dispute agent
await arbitrator . registerDisputeAgent ( "arbitrator" , getArbitratorPrivKey ( 0 ) ) ;
2022-12-13 08:44:20 +00:00
2023-02-26 10:36:14 -05:00
// create test data directory if it doesn't exist
if ( ! fs . existsSync ( TestConfig . testDataDir ) ) fs . mkdirSync ( TestConfig . testDataDir ) ;
} catch ( err ) {
await shutDown ( ) ;
throw err ;
}
2021-09-17 09:33:58 -04:00
} ) ;
2021-09-12 09:39:21 -04:00
2022-05-04 18:14:28 -04: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 ( ) = > {
2023-02-26 10:36:14 -05:00
await shutDown ( ) ;
} ) ;
async function shutDown() {
2022-12-13 08:44:20 +00:00
2022-05-10 09:55:41 -04:00
// release haveno processes
2022-10-14 16:09:10 -04:00
const promises : Promise < void > [ ] = [ ] ;
2022-05-04 18:14:28 -04:00
for ( const havenod of startupHavenods ) {
2022-05-10 09:55:41 -04:00
promises . push ( havenod . getProcess ( ) ? releaseHavenoProcess ( havenod ) : havenod . disconnect ( ) ) ;
2022-05-04 18:14:28 -04:00
}
2022-05-10 09:55:41 -04:00
await Promise . all ( promises ) ;
2022-12-13 08:44:20 +00:00
2023-10-11 09:03:25 -04:00
// terminate monero-ts worker
2023-10-02 08:16:54 -04:00
await moneroTs . LibraryUtils . terminateWorker ( ) ;
2023-02-26 10:36:14 -05:00
}
2021-12-16 20:10:40 -05:00
// ----------------------------------- TESTS ----------------------------------
2022-12-13 08:44:20 +00:00
test ( "Can get the version (CI)" , async ( ) = > {
2022-05-01 13:30:11 -04:00
const version = await arbitrator . getVersion ( ) ;
2021-12-16 20:10:40 -05:00
expect ( version ) . toEqual ( TestConfig . haveno . version ) ;
2021-09-12 09:39:21 -04:00
} ) ;
2023-05-02 09:12:43 -04:00
test ( "Can convert between XMR and atomic units (CI)" , async ( ) = > {
expect ( BigInt ( 250000000000 ) ) . toEqual ( HavenoUtils . xmrToAtomicUnits ( 0.25 ) ) ;
expect ( HavenoUtils . atomicUnitsToXmr ( "250000000000" ) ) . toEqual ( . 25 ) ;
2023-12-27 16:58:30 -05:00
expect ( HavenoUtils . atomicUnitsToXmr ( 250000000000 n ) ) . toEqual ( . 25 ) ;
2023-05-02 09:12:43 -04:00
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can manage an account (CI)" , async ( ) = > {
2022-10-14 16:09:10 -04:00
let user3 : HavenoClient | undefined ;
2022-02-09 01:41:00 -08:00
let err : any ;
2022-01-08 05:21:32 -08:00
try {
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// start user3 without opening account
user3 = await initHaveno ( { autoLogin : false } ) ;
assert ( ! await user3 . accountExists ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// test errors when account not open
2022-07-15 10:09:56 -04:00
await testAccountNotOpen ( user3 ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// create account
let password = "testPassword" ;
2022-07-15 10:09:56 -04:00
await user3 . createAccount ( password ) ;
if ( await user3 . isConnectedToMonero ( ) ) await user3 . getBalances ( ) ; // only connected if local node running
assert ( await user3 . accountExists ( ) ) ;
assert ( await user3 . isAccountOpen ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-04-10 17:00:15 -04:00
// create payment account
2022-07-15 10:09:56 -04:00
const paymentAccount = await user3 . createCryptoPaymentAccount ( "My ETH account" , TestConfig . cryptoAddresses [ 0 ] . currencyCode , TestConfig . cryptoAddresses [ 0 ] . address ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// close account
2022-07-15 10:09:56 -04:00
await user3 . closeAccount ( ) ;
assert ( await user3 . accountExists ( ) ) ;
assert ( ! await user3 . isAccountOpen ( ) ) ;
await testAccountNotOpen ( user3 ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// open account with wrong password
try {
2022-07-15 10:09:56 -04:00
await user3 . openAccount ( "wrongPassword" ) ;
2022-02-09 01:41:00 -08:00
throw new Error ( "Should have failed opening account with wrong password" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-02-09 01:41:00 -08:00
assert . equal ( err . message , "Incorrect password" ) ;
}
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// open account
2022-07-15 10:09:56 -04:00
await user3 . openAccount ( password ) ;
assert ( await user3 . accountExists ( ) ) ;
assert ( await user3 . isAccountOpen ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// restart user3
const user3Config = { appName : user3.getAppName ( ) , autoLogin : false }
await releaseHavenoProcess ( user3 ) ;
user3 = await initHaveno ( user3Config ) ;
assert ( await user3 . accountExists ( ) ) ;
assert ( ! await user3 . isAccountOpen ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// open account
2022-07-15 10:09:56 -04:00
await user3 . openAccount ( password ) ;
assert ( await user3 . accountExists ( ) ) ;
assert ( await user3 . isAccountOpen ( ) ) ;
2022-12-13 08:44:20 +00:00
2023-02-21 10:55:28 -05:00
// try changing incorrect password
try {
await user3 . changePassword ( "wrongPassword" , "abc123" ) ;
throw new Error ( "Should have failed changing wrong password" ) ;
} catch ( err : any ) {
assert . equal ( err . message , "Incorrect password" ) ;
}
2023-02-26 11:00:32 -05:00
// try setting password below minimum length
try {
await user3 . changePassword ( password , "abc123" ) ;
throw new Error ( "Should have failed setting password below minimum length" )
} catch ( err : any ) {
assert . equal ( err . message , "Password must be at least 8 characters" ) ;
}
2022-02-09 01:41:00 -08:00
// change password
2023-02-21 10:55:28 -05:00
const newPassword = "newPassword" ;
await user3 . changePassword ( password , newPassword ) ;
password = newPassword ;
2022-07-15 10:09:56 -04:00
assert ( await user3 . accountExists ( ) ) ;
assert ( await user3 . isAccountOpen ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// restart user3
await releaseHavenoProcess ( user3 ) ;
user3 = await initHaveno ( user3Config ) ;
await testAccountNotOpen ( user3 ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// open account
2022-07-15 10:09:56 -04:00
await user3 . openAccount ( password ) ;
assert ( await user3 . accountExists ( ) ) ;
assert ( await user3 . isAccountOpen ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// backup account to zip file
2022-05-01 13:30:11 -04:00
const zipFile = TestConfig . testDataDir + "/backup.zip" ;
const stream = fs . createWriteStream ( zipFile ) ;
2022-07-15 10:09:56 -04:00
const size = await user3 . backupAccount ( stream ) ;
2022-02-09 01:41:00 -08:00
stream . end ( ) ;
assert ( size > 0 ) ;
2022-12-13 08:44:20 +00:00
2023-04-15 16:12:20 -04:00
// delete account and wait until connected
2022-06-05 21:45:11 -07:00
await user3 . deleteAccount ( ) ;
2023-04-15 16:12:20 -04:00
HavenoUtils . log ( 1 , "Waiting to be connected to havenod after deleting account" ) ; // TODO: build this into deleteAccount
2022-06-05 21:45:11 -07:00
do { await wait ( 1000 ) ; }
while ( ! await user3 . isConnectedToDaemon ( ) ) ;
2023-04-15 16:12:20 -04:00
HavenoUtils . log ( 1 , "Reconnecting to havenod" ) ;
2022-06-05 21:45:11 -07:00
assert ( ! await user3 . accountExists ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-06-05 21:45:11 -07:00
// restore account
2022-05-01 13:30:11 -04:00
const zipBytes : Uint8Array = new Uint8Array ( fs . readFileSync ( zipFile ) ) ;
2022-07-15 10:09:56 -04:00
await user3 . restoreAccount ( zipBytes ) ;
2022-06-05 21:45:11 -07:00
do { await wait ( 1000 ) ; }
while ( ! await user3 . isConnectedToDaemon ( ) ) ;
assert ( await user3 . accountExists ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// open restored account
2022-07-15 10:09:56 -04:00
await user3 . openAccount ( password ) ;
assert ( await user3 . isAccountOpen ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-04-10 17:00:15 -04:00
// check the persisted payment account
2022-07-15 10:09:56 -04:00
const paymentAccount2 = await user3 . getPaymentAccount ( paymentAccount . getId ( ) ) ;
2022-04-10 17:00:15 -04:00
testCryptoPaymentAccountsEqual ( paymentAccount , paymentAccount2 ) ;
2022-02-09 01:41:00 -08:00
} catch ( err2 ) {
err = err2 ;
2022-01-09 17:02:43 +01:00
}
2022-01-24 19:37:18 +01:00
2022-04-27 12:24:55 -04:00
// stop and delete instances
2022-07-15 10:09:56 -04:00
if ( user3 ) await releaseHavenoProcess ( user3 , true ) ;
2022-02-09 01:41:00 -08:00
if ( err ) throw err ;
2022-12-13 08:44:20 +00:00
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" ) ; }
2022-05-01 13:30:11 -04:00
catch ( err : any ) { assert . equal ( err . message , "Account not open" ) ; }
2022-02-09 01:41:00 -08:00
try { await havenod . getXmrTxs ( ) ; throw new Error ( "Should have thrown" ) ; }
2022-05-01 13:30:11 -04:00
catch ( err : any ) { assert . equal ( err . message , "Account not open" ) ; }
2022-02-09 01:41:00 -08:00
try { await havenod . getBalances ( ) ; throw new Error ( "Should have thrown" ) ; }
2022-05-01 13:30:11 -04:00
catch ( err : any ) { assert . equal ( err . message , "Account not open" ) ; }
2022-04-10 17:00:15 -04:00
try { await havenod . createCryptoPaymentAccount ( "My ETH account" , TestConfig . cryptoAddresses [ 0 ] . currencyCode , TestConfig . cryptoAddresses [ 0 ] . address ) ; throw new Error ( "Should have thrown" ) ; }
2022-05-01 13:30:11 -04:00
catch ( err : any ) { assert . equal ( err . message , "Account not open" ) ; }
2022-01-09 17:02:43 +01:00
}
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can manage Monero daemon connections (CI)" , async ( ) = > {
2024-01-17 11:22:12 -05:00
let monerod3 : moneroTs.MoneroDaemonRpc | undefined = undefined ;
2022-10-14 16:09:10 -04:00
let user3 : HavenoClient | undefined ;
2022-01-24 19:37:18 +01:00
let err : any ;
try {
2022-07-15 10:09:56 -04:00
// start user3
user3 = await initHaveno ( ) ;
2022-01-24 19:37:18 +01:00
2024-10-11 11:12:52 -04:00
// disable auto switch for tests
assert . equal ( true , await user3 . getAutoSwitch ( ) ) ;
await user3 . setAutoSwitch ( false ) ;
2022-01-24 19:37:18 +01:00
// test default connections
2022-07-07 09:11:50 -04:00
const monerodUrl1 = "http://127.0.0.1:" + getNetworkStartPort ( ) + "8081" ; // TODO: (woodser): move to config
2022-07-15 10:09:56 -04:00
let connections : UrlConnection [ ] = await user3 . getMoneroConnections ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( getConnection ( connections , monerodUrl1 ) ! , monerodUrl1 , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
2022-01-24 19:37:18 +01:00
// test default connection
2022-10-14 16:09:10 -04:00
let connection : UrlConnection | undefined = await user3 . getMoneroConnection ( ) ;
2022-07-15 10:09:56 -04:00
assert ( await user3 . isConnectedToMonero ( ) ) ;
2022-07-07 09:11:50 -04:00
testConnection ( connection ! , monerodUrl1 , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ; // TODO: should be no authentication?
2022-01-24 19:37:18 +01:00
// add a new connection
2022-05-01 13:30:11 -04:00
const fooBarUrl = "http://foo.bar" ;
2022-07-15 10:09:56 -04:00
await user3 . addMoneroConnection ( fooBarUrl ) ;
connections = await user3 . 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-07-15 10:09:56 -04:00
await user3 . setMoneroConnection ( new UrlConnection ( )
2024-01-17 11:22:12 -05:00
. setUrl ( TestConfig . monerod3 . url )
2022-01-24 19:37:18 +01:00
. setPriority ( 1 ) ) ;
2022-07-15 10:09:56 -04:00
connection = await user3 . getMoneroConnection ( ) ;
2024-01-17 11:22:12 -05:00
testConnection ( connection ! , TestConfig . monerod3 . 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
2022-07-15 10:09:56 -04:00
connection = await user3 . checkMoneroConnection ( ) ;
assert ( ! await user3 . isConnectedToMonero ( ) ) ;
2024-01-17 11:22:12 -05:00
testConnection ( connection ! , TestConfig . monerod3 . url , OnlineStatus . OFFLINE , AuthenticationStatus . NO_AUTHENTICATION , 1 ) ;
2022-01-24 19:37:18 +01:00
2024-01-17 11:22:12 -05:00
// start monerod3
2022-05-01 13:30:11 -04:00
const cmd = [
2022-01-24 19:37:18 +01:00
TestConfig . moneroBinsDir + "/monerod" ,
"--no-igd" ,
"--hide-my-port" ,
2024-01-17 11:22:12 -05:00
"--data-dir" , TestConfig . moneroBinsDir + "/" + TestConfig . baseCurrencyNetwork . toLowerCase ( ) + "/node3" ,
"--p2p-bind-ip" , "127.0.0.1" ,
"--p2p-bind-port" , TestConfig . monerod3 . p2pBindPort ,
"--rpc-bind-port" , TestConfig . monerod3 . rpcBindPort ,
"--zmq-rpc-bind-port" , TestConfig . monerod3 . zmqRpcBindPort ,
"--log-level" , "0" ,
"--confirm-external-bind" ,
"--rpc-access-control-origins" , "http://localhost:8080" ,
"--fixed-difficulty" , "500" ,
"--disable-rpc-ban"
2022-01-24 19:37:18 +01:00
] ;
2023-10-02 08:16:54 -04:00
if ( getBaseCurrencyNetwork ( ) !== BaseCurrencyNetwork . XMR_MAINNET ) cmd . push ( "--" + moneroTs . MoneroNetworkType . toString ( TestConfig . networkType ) . toLowerCase ( ) ) ;
2024-01-17 11:22:12 -05:00
if ( TestConfig . monerod3 . username ) cmd . push ( "--rpc-login" , TestConfig . monerod3 . username + ":" + TestConfig . monerod3 . password ) ;
monerod3 = await moneroTs . connectToDaemonRpc ( cmd ) ;
2022-01-24 19:37:18 +01:00
// connection is online and not authenticated
2022-07-15 10:09:56 -04:00
connection = await user3 . checkMoneroConnection ( ) ;
assert ( ! await user3 . isConnectedToMonero ( ) ) ;
2024-01-17 11:22:12 -05:00
testConnection ( connection ! , TestConfig . monerod3 . url , OnlineStatus . ONLINE , AuthenticationStatus . NOT_AUTHENTICATED , 1 ) ;
2022-01-24 19:37:18 +01:00
// set connection credentials
2022-07-15 10:09:56 -04:00
await user3 . setMoneroConnection ( new UrlConnection ( )
2024-01-17 11:22:12 -05:00
. setUrl ( TestConfig . monerod3 . url )
. setUsername ( TestConfig . monerod3 . username )
. setPassword ( TestConfig . monerod3 . password )
2022-01-24 19:37:18 +01:00
. setPriority ( 1 ) ) ;
2022-07-15 10:09:56 -04:00
connection = await user3 . getMoneroConnection ( ) ;
2024-01-17 11:22:12 -05:00
testConnection ( connection ! , TestConfig . monerod3 . url , undefined , undefined , 1 ) ;
2022-01-24 19:37:18 +01:00
// connection is online and authenticated
2022-07-15 10:09:56 -04:00
connection = await user3 . checkMoneroConnection ( ) ;
assert ( await user3 . isConnectedToMonero ( ) ) ;
2024-01-17 11:22:12 -05:00
testConnection ( connection ! , TestConfig . monerod3 . url , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// change account password
2023-02-21 10:55:28 -05:00
const newPassword = "newPassword" ;
await user3 . changePassword ( TestConfig . defaultHavenod . accountPassword , newPassword ) ;
2022-01-24 19:37:18 +01:00
2022-07-15 10:09:56 -04:00
// restart user3
const appName = user3 . getAppName ( ) ;
await releaseHavenoProcess ( user3 ) ;
2023-02-21 10:55:28 -05:00
user3 = await initHaveno ( { appName : appName , accountPassword : newPassword } ) ;
2022-01-24 19:37:18 +01:00
// connection is restored, online, and authenticated
2024-04-23 10:44:13 -04:00
await user3 . checkMoneroConnection ( ) ;
2022-07-15 10:09:56 -04:00
connection = await user3 . getMoneroConnection ( ) ;
2024-01-17 11:22:12 -05:00
testConnection ( connection ! , TestConfig . monerod3 . url , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
2023-09-11 09:30:05 -04:00
// priority connections are polled
2024-04-23 10:44:13 -04:00
await user3 . checkMoneroConnections ( ) ;
2022-07-15 10:09:56 -04:00
connections = await user3 . getMoneroConnections ( ) ;
2023-09-05 18:26:50 -04:00
testConnection ( getConnection ( connections , monerodUrl1 ) ! , monerodUrl1 , OnlineStatus . ONLINE , AuthenticationStatus . AUTHENTICATED , 1 ) ;
2022-01-24 19:37:18 +01:00
// enable auto switch
2022-07-15 10:09:56 -04:00
await user3 . setAutoSwitch ( true ) ;
2024-10-07 09:22:36 -04:00
assert . equal ( true , await user3 . getAutoSwitch ( ) ) ;
2022-01-24 19:37:18 +01:00
// stop monerod
2024-01-17 11:22:12 -05:00
//await monerod3.stopProcess(); // TODO (monero-ts): monerod remains available after await monerod.stopProcess() for up to 40 seconds
await moneroTs . GenUtils . killProcess ( monerod3 . getProcess ( ) , "SIGKILL" ) ;
2022-01-24 19:37:18 +01:00
// test auto switch after periodic connection check
2022-06-16 21:51:22 -04:00
await wait ( TestConfig . daemonPollPeriodMs * 2 ) ;
2024-04-23 10:44:13 -04:00
await user3 . checkMoneroConnection ( ) ;
2022-07-15 10:09:56 -04:00
connection = await user3 . 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
2022-07-07 09:11:50 -04:00
// stop auto switch and checking connection periodically
2022-07-15 10:09:56 -04:00
await user3 . setAutoSwitch ( false ) ;
2024-10-07 09:22:36 -04:00
assert . equal ( false , await user3 . getAutoSwitch ( ) ) ;
2022-07-15 10:09:56 -04:00
await user3 . stopCheckingConnection ( ) ;
2022-01-24 19:37:18 +01:00
// remove current connection
2022-07-15 10:09:56 -04:00
await user3 . removeMoneroConnection ( monerodUrl1 ) ;
2022-01-24 19:37:18 +01:00
// check current connection
2022-07-15 10:09:56 -04:00
connection = await user3 . checkMoneroConnection ( ) ;
2022-02-09 01:41:00 -08:00
assert . equal ( connection , undefined ) ;
2022-01-24 19:37:18 +01:00
// check all connections
2022-07-15 10:09:56 -04:00
await user3 . checkMoneroConnections ( ) ;
connections = await user3 . getMoneroConnections ( ) ;
2022-02-09 01:41:00 -08:00
testConnection ( getConnection ( connections , fooBarUrl ) ! , 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 previous url
2022-07-15 10:09:56 -04:00
await user3 . setMoneroConnection ( fooBarUrl ) ;
connection = await user3 . 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
2022-05-01 13:30:11 -04:00
const fooBarUrl2 = "http://foo.bar.xyz" ;
2022-07-15 10:09:56 -04:00
await user3 . setMoneroConnection ( fooBarUrl2 ) ;
connections = await user3 . 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
2022-07-15 10:09:56 -04:00
await user3 . setMoneroConnection ( ) ;
assert . equal ( await user3 . getMoneroConnection ( ) , undefined ) ;
2022-01-24 19:37:18 +01:00
// test auto switch after start checking connection
2022-07-15 10:09:56 -04:00
await user3 . setAutoSwitch ( false ) ;
await user3 . startCheckingConnection ( 5000 ) ; // checks the connection
await user3 . setAutoSwitch ( true ) ;
await user3 . addMoneroConnection ( new UrlConnection ( )
2022-02-09 01:41:00 -08:00
. 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 ) ;
2022-07-15 10:09:56 -04:00
connection = await user3 . 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-07-15 10:09:56 -04:00
if ( user3 ) await releaseHavenoProcess ( user3 , true ) ;
2024-01-17 11:22:12 -05:00
if ( monerod3 ) await monerod3 . stopProcess ( ) ;
2022-01-24 19:37:18 +01:00
if ( err ) throw err ;
2021-10-22 13:51:57 -04:00
} ) ;
2024-01-17 11:22:12 -05:00
// NOTE: To run full test, the following conditions must be met:
// - monerod1-local must be stopped
// - monerod2-local must be running
// - user1-daemon-local must be running and own its monerod process (so it can be stopped)
2022-12-13 08:44:20 +00:00
test ( "Can start and stop a local Monero node (CI)" , async ( ) = > {
2022-04-04 12:29:35 -07:00
2024-10-12 06:35:15 -04:00
// expect error stopping stopped local node
2022-12-13 08:44:20 +00:00
try {
2022-07-15 10:09:56 -04:00
await user1 . stopMoneroNode ( ) ;
2022-04-04 12:29:35 -07:00
HavenoUtils . log ( 1 , "Running local Monero node stopped" ) ;
2022-07-15 10:09:56 -04:00
await user1 . stopMoneroNode ( ) ; // stop 2nd time to force error
2022-04-04 12:29:35 -07:00
throw new Error ( "should have thrown" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-04-04 12:29:35 -07:00
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 ) ;
}
}
2024-10-12 06:35:15 -04:00
if ( await user1 . isMoneroNodeOnline ( ) ) {
2022-04-04 12:29:35 -07:00
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
2023-11-25 14:48:58 -05:00
const newSettings = new XmrNodeSettings ( ) ;
2022-04-04 12:29:35 -07:00
try {
2022-07-15 10:09:56 -04:00
await user1 . startMoneroNode ( newSettings ) ;
2022-04-04 12:29:35 -07:00
throw new Error ( "should have thrown" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-07-15 10:22:19 -04:00
if ( err . message !== "Local Monero node already online" ) throw new Error ( "Unexpected error: " + err . message ) ;
2022-04-04 12:29:35 -07:00
}
} else {
// expect error when passing in bad arguments
2023-11-25 14:48:58 -05:00
const badSettings = new XmrNodeSettings ( ) ;
2022-04-04 12:29:35 -07:00
badSettings . setStartupFlagsList ( [ "--invalid-flag" ] ) ;
try {
2022-07-15 10:09:56 -04:00
await user1 . startMoneroNode ( badSettings ) ;
2022-04-04 12:29:35 -07:00
throw new Error ( "should have thrown" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-04-04 12:29:35 -07:00
if ( ! err . message . startsWith ( "Failed to start monerod:" ) ) throw new Error ( "Unexpected error: " ) ;
}
// expect successful start with custom settings
2022-07-15 10:09:56 -04:00
const connectionsBefore = await user1 . getMoneroConnections ( ) ;
2023-11-25 14:48:58 -05:00
const settings : XmrNodeSettings = new XmrNodeSettings ( ) ;
2024-01-17 11:22:12 -05:00
const dataDir = TestConfig . moneroBinsDir + "/" + TestConfig . baseCurrencyNetwork . toLowerCase ( ) + "/node1" ;
2022-05-01 13:30:11 -04:00
const logFile = dataDir + "/test.log" ;
2022-04-04 12:29:35 -07:00
settings . setBlockchainPath ( dataDir ) ;
2024-01-17 11:22:12 -05:00
settings . setStartupFlagsList ( [ "--log-file" , logFile , "--no-zmq" ] ) ;
2022-07-15 10:09:56 -04:00
await user1 . startMoneroNode ( settings ) ;
2024-10-12 06:35:15 -04:00
assert ( await user1 . isMoneroNodeOnline ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-04-04 12:29:35 -07:00
// expect settings are updated
2022-07-15 10:09:56 -04:00
const settingsAfter = await user1 . getMoneroNodeSettings ( ) ;
2022-04-10 17:00:15 -04:00
testMoneroNodeSettingsEqual ( settings , settingsAfter ! ) ;
2022-04-04 12:29:35 -07:00
// expect connection to local monero node to succeed
2024-01-17 11:22:12 -05:00
let daemon = await moneroTs . connectToDaemonRpc ( TestConfig . monerod . url , "superuser" , "abctesting123" ) ;
2022-04-04 12:29:35 -07:00
let height = await daemon . getHeight ( ) ;
2024-01-17 11:22:12 -05:00
assert ( height > 0 ) ;
2022-04-04 12:29:35 -07:00
// expect error due to existing running node
2023-11-25 14:48:58 -05:00
const newSettings = new XmrNodeSettings ( ) ;
2022-04-04 12:29:35 -07:00
try {
2022-07-15 10:09:56 -04:00
await user1 . startMoneroNode ( newSettings ) ;
2022-04-04 12:29:35 -07:00
throw new Error ( "should have thrown" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-11-04 15:57:42 -04:00
if ( err . message !== "Local Monero node already online" ) throw new Error ( "Unexpected error: " + err . message ) ;
2022-04-04 12:29:35 -07:00
}
// expect stopped node
2022-07-15 10:09:56 -04:00
await user1 . stopMoneroNode ( ) ;
2024-10-12 06:35:15 -04:00
assert ( ! ( await user1 . isMoneroNodeOnline ( ) ) ) ;
2022-04-04 12:29:35 -07:00
try {
2024-01-17 11:22:12 -05:00
daemon = await moneroTs . connectToDaemonRpc ( TestConfig . monerod . url ) ;
2022-04-04 12:29:35 -07:00
height = await daemon . getHeight ( ) ;
2024-01-17 11:22:12 -05:00
console . log ( "GOT HEIGHT: " + height ) ;
throw new Error ( "should have thrown" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2024-10-12 06:35:15 -04:00
if ( err . message . indexOf ( "connect ECONNREFUSED 127.0.0.1:28081" ) <= 0 ) throw new Error ( "Unexpected error: " + err . message ) ;
2022-04-04 12:29:35 -07:00
}
2024-10-12 06:35:15 -04:00
// start local node again
await user1 . startMoneroNode ( settings ) ;
assert ( await user1 . isMoneroNodeOnline ( ) ) ;
2022-04-04 12:29:35 -07:00
}
} ) ;
2021-12-30 22:03:00 +02:00
// test wallet balances, transactions, deposit addresses, create and relay txs
2022-12-13 08:44:20 +00:00
test ( "Has a Monero wallet (CI)" , async ( ) = > {
2022-05-10 14:53:10 -04:00
// get seed phrase
2022-07-15 10:09:56 -04:00
const seed = await user1 . getXmrSeed ( ) ;
2023-10-02 08:16:54 -04:00
await moneroTs . MoneroUtils . validateMnemonic ( seed ) ;
2022-12-13 08:44:20 +00:00
2022-05-15 13:46:56 -04:00
// get primary address
2022-07-15 10:09:56 -04:00
const primaryAddress = await user1 . getXmrPrimaryAddress ( ) ;
2023-10-02 08:16:54 -04:00
await moneroTs . MoneroUtils . validateAddress ( primaryAddress , TestConfig . networkType ) ;
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// wait for user1 to have unlocked balance
2023-12-27 16:58:30 -05:00
const tradeAmount = 250000000000 n ;
await waitForAvailableBalance ( tradeAmount * 2 n , user1 ) ;
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// test balances
2022-07-15 10:09:56 -04:00
const balancesBefore : XmrBalanceInfo = await user1 . getBalances ( ) ; // TODO: rename to getXmrBalances() for consistency?
2023-12-27 16:58:30 -05:00
expect ( BigInt ( balancesBefore . getAvailableBalance ( ) ) ) . toBeGreaterThan ( 0 n ) ;
2022-08-17 12:22:08 -04:00
expect ( BigInt ( balancesBefore . getBalance ( ) ) ) . toBeGreaterThanOrEqual ( BigInt ( balancesBefore . getAvailableBalance ( ) ) ) ;
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// get transactions
2022-07-15 10:09:56 -04:00
const txs : XmrTx [ ] = await user1 . getXmrTxs ( ) ;
2021-12-30 22:03:00 +02:00
assert ( txs . length > 0 ) ;
2022-05-01 13:30:11 -04:00
for ( const tx of txs ) {
2021-12-30 22:03:00 +02:00
testTx ( tx , { isCreatedTx : false } ) ;
}
2022-12-13 08:44:20 +00:00
2022-05-15 13:46:56 -04:00
// get new subaddresses
2021-12-30 22:03:00 +02:00
for ( let i = 0 ; i < 0 ; i ++ ) {
2022-07-15 10:09:56 -04:00
const address = await user1 . getXmrNewSubaddress ( ) ;
2023-10-02 08:16:54 -04:00
await moneroTs . MoneroUtils . validateAddress ( address , TestConfig . networkType ) ;
2021-12-30 22:03:00 +02:00
}
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// create withdraw tx
2022-07-15 10:09:56 -04:00
const destination = new XmrDestination ( ) . setAddress ( await user1 . getXmrNewSubaddress ( ) ) . setAmount ( "100000000000" ) ;
2022-11-27 18:31:33 +00:00
let tx : XmrTx | undefined = await user1 . createXmrTx ( [ destination ] ) ;
2021-12-30 22:03:00 +02:00
testTx ( tx , { isCreatedTx : true } ) ;
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// relay withdraw tx
2022-07-15 10:09:56 -04:00
const txHash = await user1 . relayXmrTx ( tx . getMetadata ( ) ) ;
2021-12-30 22:03:00 +02:00
expect ( txHash . length ) . toEqual ( 64 ) ;
2024-08-06 11:45:09 -04:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ; // wait for wallet to sync relayed tx
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// balances decreased
2022-07-15 10:09:56 -04:00
const balancesAfter = await user1 . getBalances ( ) ;
2021-12-30 22:03:00 +02:00
expect ( BigInt ( balancesAfter . getBalance ( ) ) ) . toBeLessThan ( BigInt ( balancesBefore . getBalance ( ) ) ) ;
2022-08-17 12:22:08 -04:00
expect ( BigInt ( balancesAfter . getAvailableBalance ( ) ) ) . toBeLessThan ( BigInt ( balancesBefore . getAvailableBalance ( ) ) ) ;
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// get relayed tx
2022-07-15 10:09:56 -04:00
tx = await user1 . getXmrTx ( txHash ) ;
2022-11-27 18:31:33 +00:00
testTx ( tx ! , { isCreatedTx : false } ) ;
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// relay invalid tx
try {
2022-07-15 10:09:56 -04:00
await user1 . relayXmrTx ( "invalid tx metadata" ) ;
2021-12-30 22:03:00 +02:00
throw new Error ( "Cannot relay invalid tx metadata" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2021-12-30 22:03:00 +02:00
if ( err . message !== "Failed to parse hex." ) throw new Error ( "Unexpected error: " + err . message ) ;
}
} ) ;
2022-12-23 09:32:42 +00:00
test ( "Can get balances (CI, sanity check)" , async ( ) = > {
2022-07-15 10:09:56 -04:00
const balances : XmrBalanceInfo = await user1 . getBalances ( ) ;
2022-08-17 12:22:08 -04:00
expect ( BigInt ( balances . getAvailableBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
expect ( BigInt ( balances . getPendingBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
2021-11-11 13:48:31 -05:00
expect ( BigInt ( balances . getReservedOfferBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
expect ( BigInt ( balances . getReservedTradeBalance ( ) ) ) . toBeGreaterThanOrEqual ( 0 ) ;
2021-09-12 09:39:21 -04:00
} ) ;
2022-12-29 11:20:57 +00:00
test ( "Can send and receive push notifications (CI, sanity check)" , async ( ) = > {
2022-02-09 01:41:00 -08:00
// add notification listener
2022-05-01 13:30:11 -04:00
const notifications : NotificationMessage [ ] = [ ] ;
2022-07-15 10:09:56 -04:00
await user1 . addNotificationListener ( notification = > {
2022-02-09 01:41:00 -08:00
notifications . push ( notification ) ;
} ) ;
// send test notification
for ( let i = 0 ; i < 3 ; i ++ ) {
2022-07-15 10:09:56 -04:00
await user1 . _sendNotification ( new NotificationMessage ( )
2022-02-09 01:41:00 -08:00
. 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 ) ;
}
} ) ;
2023-08-29 06:40:25 -04:00
test ( "Can get asset codes with prices and their payment methods (CI, sanity check)" , async ( ) = > {
const assetCodes = await user1 . getPricedAssetCodes ( ) ;
2022-05-11 07:53:42 -04:00
for ( const assetCode of assetCodes ) {
2022-07-15 10:09:56 -04:00
const paymentMethods = await user1 . getPaymentMethods ( assetCode ) ;
2022-05-11 07:53:42 -04:00
expect ( paymentMethods . length ) . toBeGreaterThanOrEqual ( 0 ) ;
}
} ) ;
2022-12-23 09:32:42 +00:00
test ( "Can get market prices (CI, sanity check)" , async ( ) = > {
2022-01-24 19:37:18 +01:00
// get all market prices
2022-07-15 10:09:56 -04:00
const prices : MarketPriceInfo [ ] = await user1 . getPrices ( ) ;
2022-01-24 19:37:18 +01:00
expect ( prices . length ) . toBeGreaterThan ( 1 ) ;
2022-05-01 13:30:11 -04:00
for ( const price of prices ) {
2022-01-24 19:37:18 +01:00
expect ( price . getCurrencyCode ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( price . getPrice ( ) ) . toBeGreaterThanOrEqual ( 0 ) ;
}
2022-12-13 08:44:20 +00:00
2022-02-16 10:09:59 -05:00
// get market prices of primary assets
2022-05-01 13:30:11 -04:00
for ( const assetCode of TestConfig . assetCodes ) {
2022-07-15 10:09:56 -04:00
const price = await user1 . getPrice ( assetCode ) ;
2022-01-24 19:37:18 +01:00
expect ( price ) . toBeGreaterThan ( 0 ) ;
}
2022-12-13 08:44:20 +00:00
2022-01-24 19:37:18 +01:00
// test that prices are reasonable
2022-07-15 10:09:56 -04:00
const usd = await user1 . getPrice ( "USD" ) ;
2022-01-24 19:37:18 +01:00
expect ( usd ) . toBeGreaterThan ( 50 ) ;
expect ( usd ) . toBeLessThan ( 5000 ) ;
2022-12-13 08:44:20 +00:00
const ltc = await user1 . getPrice ( "LTC" ) ;
expect ( ltc ) . toBeGreaterThan ( 0.0004 ) ;
expect ( ltc ) . toBeLessThan ( 40 ) ;
2022-07-15 10:09:56 -04:00
const btc = await user1 . getPrice ( "BTC" ) ;
2022-12-13 08:44:20 +00:00
expect ( btc ) . toBeGreaterThan ( 0.0004 ) ;
2022-01-24 19:37:18 +01:00
expect ( btc ) . toBeLessThan ( 0.4 ) ;
2022-12-13 08:44:20 +00:00
2022-01-24 19:37:18 +01:00
// test invalid currency
2022-07-15 10:09:56 -04:00
await expect ( async ( ) = > { await user1 . getPrice ( "INVALID_CURRENCY" ) } )
2022-01-24 19:37:18 +01:00
. rejects
. toThrow ( 'Currency not found: INVALID_CURRENCY' ) ;
} ) ;
2022-12-23 09:32:42 +00:00
test ( "Can get market depth (CI, sanity check)" , async ( ) = > {
2022-05-01 13:30:11 -04:00
const assetCode = "eth" ;
2022-12-13 08:44:20 +00:00
2022-02-11 17:13:56 -06:00
// clear offers
2022-07-15 10:09:56 -04:00
await clearOffers ( user1 , assetCode ) ;
await clearOffers ( user2 , assetCode ) ;
2022-04-07 16:35:48 -04:00
async function clearOffers ( havenod : HavenoClient , assetCode : string ) {
2022-05-01 13:30:11 -04:00
for ( const offer of await havenod . getMyOffers ( assetCode ) ) {
2023-03-06 13:27:52 -05:00
if ( offer . getBaseCurrencyCode ( ) . toLowerCase ( ) === assetCode . toLowerCase ( ) ) {
2022-02-11 17:13:56 -06:00
await havenod . removeOffer ( offer . getId ( ) ) ;
}
}
}
2022-12-13 08:44:20 +00:00
2022-02-11 17:13:56 -06:00
// market depth has no data
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
2022-07-15 10:09:56 -04:00
let marketDepth = await user1 . 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 ) ;
2022-12-13 08:44:20 +00:00
2022-02-11 17:13:56 -06:00
// post offers to buy and sell
2023-12-27 16:58:30 -05:00
await makeOffer ( { maker : { havenod : user1 } , direction : OfferDirection.BUY , offerAmount : 150000000000n , assetCode : assetCode , price : 17.0 } ) ;
await makeOffer ( { maker : { havenod : user1 } , direction : OfferDirection.BUY , offerAmount : 150000000000n , assetCode : assetCode , price : 17.2 } ) ;
await makeOffer ( { maker : { havenod : user1 } , direction : OfferDirection.BUY , offerAmount : 200000000000n , assetCode : assetCode , price : 17.3 } ) ;
await makeOffer ( { maker : { havenod : user1 } , direction : OfferDirection.BUY , offerAmount : 150000000000n , assetCode : assetCode , price : 17.3 } ) ;
await makeOffer ( { maker : { havenod : user1 } , direction : OfferDirection.SELL , offerAmount : 300000000000n , assetCode : assetCode , priceMargin : 0.00 } ) ;
await makeOffer ( { maker : { havenod : user1 } , direction : OfferDirection.SELL , offerAmount : 300000000000n , assetCode : assetCode , priceMargin : 0.02 } ) ;
await makeOffer ( { maker : { havenod : user1 } , direction : OfferDirection.SELL , offerAmount : 400000000000n , assetCode : assetCode , priceMargin : 0.05 } ) ;
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// get user2's market depth
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
2022-07-15 10:09:56 -04:00
marketDepth = await user1 . getMarketDepth ( assetCode ) ;
2022-12-13 08:44:20 +00:00
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 ) ;
2022-12-13 08:44:20 +00:00
2022-02-11 17:13:56 -06:00
// test buy prices and depths
2023-11-09 17:17:14 -05:00
const buyOffers = ( await user1 . getOffers ( assetCode , OfferDirection . BUY ) ) . concat ( await user1 . getMyOffers ( assetCode , OfferDirection . BUY ) ) . sort ( function ( a , b ) { return parseFloat ( a . getPrice ( ) ) - parseFloat ( b . getPrice ( ) ) } ) ;
2022-05-26 10:58:15 -04:00
expect ( marketDepth . getBuyPricesList ( ) [ 0 ] ) . toEqual ( 1 / parseFloat ( buyOffers [ 0 ] . getPrice ( ) ) ) ; // TODO: price when posting offer is reversed. this assumes crypto counter currency
expect ( marketDepth . getBuyPricesList ( ) [ 1 ] ) . toEqual ( 1 / parseFloat ( buyOffers [ 1 ] . getPrice ( ) ) ) ;
expect ( marketDepth . getBuyPricesList ( ) [ 2 ] ) . toEqual ( 1 / parseFloat ( buyOffers [ 2 ] . getPrice ( ) ) ) ;
2022-02-11 17:13:56 -06:00
expect ( marketDepth . getBuyDepthList ( ) [ 0 ] ) . toEqual ( 0.15 ) ;
expect ( marketDepth . getBuyDepthList ( ) [ 1 ] ) . toEqual ( 0.30 ) ;
expect ( marketDepth . getBuyDepthList ( ) [ 2 ] ) . toEqual ( 0.65 ) ;
2022-12-13 08:44:20 +00:00
2022-02-11 17:13:56 -06:00
// test sell prices and depths
2023-11-09 17:17:14 -05:00
const sellOffers = ( await user1 . getOffers ( assetCode , OfferDirection . SELL ) ) . concat ( await user1 . getMyOffers ( assetCode , OfferDirection . SELL ) ) . sort ( function ( a , b ) { return parseFloat ( b . getPrice ( ) ) - parseFloat ( a . getPrice ( ) ) } ) ;
2022-05-26 10:58:15 -04:00
expect ( marketDepth . getSellPricesList ( ) [ 0 ] ) . toEqual ( 1 / parseFloat ( sellOffers [ 0 ] . getPrice ( ) ) ) ;
expect ( marketDepth . getSellPricesList ( ) [ 1 ] ) . toEqual ( 1 / parseFloat ( sellOffers [ 1 ] . getPrice ( ) ) ) ;
expect ( marketDepth . getSellPricesList ( ) [ 2 ] ) . toEqual ( 1 / parseFloat ( sellOffers [ 2 ] . getPrice ( ) ) ) ;
2022-02-11 17:13:56 -06:00
expect ( marketDepth . getSellDepthList ( ) [ 0 ] ) . toEqual ( 0.3 ) ;
expect ( marketDepth . getSellDepthList ( ) [ 1 ] ) . toEqual ( 0.6 ) ;
expect ( marketDepth . getSellDepthList ( ) [ 2 ] ) . toEqual ( 1 ) ;
2022-12-13 08:44:20 +00:00
2022-02-11 17:13:56 -06:00
// clear offers
2022-07-15 10:09:56 -04:00
await clearOffers ( user1 , assetCode ) ;
await clearOffers ( user2 , assetCode ) ;
2022-12-13 08:44:20 +00:00
2022-02-11 17:13:56 -06:00
// test invalid currency
2022-07-15 10:09:56 -04:00
await expect ( async ( ) = > { await user1 . getMarketDepth ( "INVALID_CURRENCY" ) } )
2022-02-11 17:13:56 -06:00
. rejects
. toThrow ( 'Currency not found: INVALID_CURRENCY' ) ;
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can register as an arbitrator (CI)" , async ( ) = > {
2022-02-09 01:41:00 -08:00
// test bad dispute agent type
try {
2022-08-12 08:28:58 -04:00
await arbitrator . registerDisputeAgent ( "unsupported type" , getArbitratorPrivKey ( 0 ) ) ;
2022-02-09 01:41:00 -08:00
throw new Error ( "should have thrown error registering bad type" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-02-09 01:41:00 -08:00
if ( err . message !== "unknown dispute agent type 'unsupported type'" ) throw new Error ( "Unexpected error: " + err . message ) ;
}
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// test bad key
try {
2022-07-07 09:11:50 -04:00
await arbitrator . registerDisputeAgent ( "mediator" , "bad key" ) ;
2022-02-09 01:41:00 -08:00
throw new Error ( "should have thrown error registering bad key" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-02-09 01:41:00 -08:00
if ( err . message !== "invalid registration key" ) throw new Error ( "Unexpected error: " + err . message ) ;
}
2022-12-13 08:44:20 +00:00
2022-07-07 09:11:50 -04:00
// register arbitrator with good key
2022-08-12 08:28:58 -04:00
await arbitrator . registerDisputeAgent ( "arbitrator" , getArbitratorPrivKey ( 0 ) ) ;
2022-02-09 01:41:00 -08:00
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can get offers (CI)" , async ( ) = > {
2022-05-01 13:30:11 -04:00
for ( const assetCode of TestConfig . assetCodes ) {
2022-07-15 10:09:56 -04:00
const offers : OfferInfo [ ] = await user1 . getOffers ( assetCode ) ;
2022-05-01 13:30:11 -04:00
for ( const offer of offers ) testOffer ( offer ) ;
2021-09-12 09:39:21 -04:00
}
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can get my offers (CI)" , async ( ) = > {
2023-02-23 09:21:22 -05:00
// get all offers
const offers : OfferInfo [ ] = await user1 . getMyOffers ( ) ;
for ( const offer of offers ) testOffer ( offer ) ;
// get offers by asset code
2022-05-01 13:30:11 -04:00
for ( const assetCode of TestConfig . assetCodes ) {
2022-07-15 10:09:56 -04:00
const offers : OfferInfo [ ] = await user1 . getMyOffers ( assetCode ) ;
2023-02-23 09:21:22 -05:00
for ( const offer of offers ) {
testOffer ( offer ) ;
expect ( assetCode ) . toEqual ( isCrypto ( assetCode ) ? offer . getBaseCurrencyCode ( ) : offer . getCounterCurrencyCode ( ) ) ; // crypto asset codes are base
}
2022-02-16 10:09:59 -05:00
}
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can get payment methods (CI)" , async ( ) = > {
2022-07-15 10:09:56 -04:00
const paymentMethods : PaymentMethod [ ] = await user1 . getPaymentMethods ( ) ;
2022-02-16 10:09:59 -05:00
expect ( paymentMethods . length ) . toBeGreaterThan ( 0 ) ;
2022-05-01 13:30:11 -04:00
for ( const paymentMethod of paymentMethods ) {
2022-02-16 10:09:59 -05:00
expect ( paymentMethod . getId ( ) . length ) . toBeGreaterThan ( 0 ) ;
2023-12-27 16:58:30 -05:00
expect ( BigInt ( paymentMethod . getMaxTradeLimit ( ) ) ) . toBeGreaterThan ( 0 n ) ;
expect ( BigInt ( paymentMethod . getMaxTradePeriod ( ) ) ) . toBeGreaterThan ( 0 n ) ;
2022-05-11 07:53:42 -04:00
expect ( paymentMethod . getSupportedAssetCodesList ( ) . length ) . toBeGreaterThanOrEqual ( 0 ) ;
2021-09-14 08:27:45 -04:00
}
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can get payment accounts (CI)" , async ( ) = > {
2022-07-15 10:09:56 -04:00
const paymentAccounts : PaymentAccount [ ] = await user1 . getPaymentAccounts ( ) ;
2022-05-01 13:30:11 -04:00
for ( const 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-06-16 21:51:22 -04:00
// TODO: FieldId represented as number
2022-12-23 09:32:42 +00:00
test ( "Can validate payment account forms (CI, sanity check)" , async ( ) = > {
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// get payment methods
2022-07-15 10:09:56 -04:00
const paymentMethods = await user1 . getPaymentMethods ( ) ;
2023-10-10 07:02:36 -04:00
expect ( paymentMethods . length ) . toEqual ( TestConfig . paymentMethods . length ) ;
2022-06-16 21:51:22 -04:00
for ( const paymentMethod of paymentMethods ) {
2023-10-10 07:02:36 -04:00
assert ( moneroTs . GenUtils . arrayContains ( TestConfig . paymentMethods , paymentMethod . getId ( ) ) , "Payment method is not expected: " + paymentMethod . getId ( ) ) ;
2022-06-16 21:51:22 -04:00
}
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// test form for each payment method
for ( const paymentMethod of paymentMethods ) {
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// generate form
2022-07-15 10:09:56 -04:00
const accountForm = await user1 . getPaymentAccountForm ( paymentMethod . getId ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// complete form, validating each field
for ( const field of accountForm . getFieldsList ( ) ) {
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// validate invalid form field
try {
const invalidInput = getInvalidFormInput ( accountForm , field . getId ( ) ) ;
2022-07-15 10:09:56 -04:00
await user1 . validateFormField ( accountForm , field . getId ( ) , invalidInput ) ;
2022-06-16 21:51:22 -04:00
throw new Error ( "Should have thrown error validating form field '" + field . getId ( ) + "' with invalid value '" + invalidInput + "'" ) ;
} catch ( err : any ) {
if ( err . message . indexOf ( "Not implemented" ) >= 0 ) throw err ;
if ( err . message . indexOf ( "Should have thrown" ) >= 0 ) throw err ;
}
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// validate valid form field
2023-08-26 06:55:56 -04:00
const validInput = getValidFormInput ( accountForm , field . getId ( ) ) ;
2022-07-15 10:09:56 -04:00
await user1 . validateFormField ( accountForm , field . getId ( ) , validInput ) ;
2022-06-16 21:51:22 -04:00
field . setValue ( validInput ) ;
}
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// create payment account
2022-11-23 09:41:48 +00:00
const paymentAccount = await user1 . createPaymentAccount ( accountForm ) ;
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// payment account added
let found = false ;
2022-11-23 09:41:48 +00:00
for ( const userAccount of await user1 . getPaymentAccounts ( ) ) {
if ( paymentAccount . getId ( ) === userAccount . getId ( ) ) {
2022-06-16 21:51:22 -04:00
found = true ;
break ;
}
}
assert ( found , "Payment account not found after adding" ) ;
2022-12-13 08:44:20 +00:00
2022-06-16 21:51:22 -04:00
// test payment account
2022-11-23 09:41:48 +00:00
expect ( paymentAccount . getPaymentMethod ( ) ! . getId ( ) ) . toEqual ( paymentMethod . getId ( ) ) ;
testPaymentAccount ( paymentAccount , accountForm ) ;
2023-10-10 07:02:36 -04:00
// delete payment account
// await user1.deletePaymentAccount(paymentAccount.getId()); // TODO: support deleting payment accounts over grpc
2022-06-16 21:51:22 -04:00
}
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can create fiat payment accounts (CI)" , async ( ) = > {
2022-02-16 10:09:59 -05:00
// get payment account form
2023-08-29 10:32:50 -04:00
const paymentMethodId = HavenoUtils . getPaymentMethodId ( PaymentAccountForm . FormId . REVOLUT ) ;
2022-07-15 10:09:56 -04:00
const accountForm = await user1 . getPaymentAccountForm ( paymentMethodId ) ;
2022-12-13 08:44:20 +00:00
2022-02-16 10:09:59 -05:00
// edit form
2023-11-01 06:39:35 -04:00
HavenoUtils . setFormValue ( accountForm , PaymentAccountFormField . FieldId . ACCOUNT_NAME , "Revolut account " + moneroTs . GenUtils . getUUID ( ) ) ;
2024-06-08 10:50:50 -04:00
HavenoUtils . setFormValue ( accountForm , PaymentAccountFormField . FieldId . USERNAME , "user123" ) ;
2023-11-01 06:39:35 -04:00
HavenoUtils . setFormValue ( accountForm , PaymentAccountFormField . FieldId . TRADE_CURRENCIES , "gbp,eur,usd" ) ;
2022-12-13 08:44:20 +00:00
2022-02-16 10:09:59 -05:00
// create payment account
2022-07-15 10:09:56 -04:00
const fiatAccount = await user1 . createPaymentAccount ( accountForm ) ;
2022-11-23 09:41:48 +00:00
expect ( fiatAccount . getAccountName ( ) ) . toEqual ( HavenoUtils . getFormValue ( accountForm , PaymentAccountFormField . FieldId . ACCOUNT_NAME ) ) ;
2022-10-14 16:09:10 -04:00
expect ( fiatAccount . getSelectedTradeCurrency ( ) ! . getCode ( ) ) . toEqual ( "USD" ) ;
2022-02-16 10:09:59 -05:00
expect ( fiatAccount . getTradeCurrenciesList ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( fiatAccount . getPaymentAccountPayload ( ) ! . getPaymentMethodId ( ) ) . toEqual ( paymentMethodId ) ;
2024-06-08 10:50:50 -04:00
expect ( fiatAccount . getPaymentAccountPayload ( ) ! . getRevolutAccountPayload ( ) ! . getUsername ( ) ) . toEqual ( HavenoUtils . getFormValue ( accountForm , PaymentAccountFormField . FieldId . USERNAME ) ) ;
2022-12-13 08:44:20 +00:00
2022-02-16 10:09:59 -05:00
// payment account added
let found = false ;
2022-07-15 10:09:56 -04:00
for ( const paymentAccount of await user1 . getPaymentAccounts ( ) ) {
2022-02-16 10:09:59 -05:00
if ( paymentAccount . getId ( ) === fiatAccount . getId ( ) ) {
2022-06-16 21:51:22 -04:00
found = true ;
break ;
2022-02-16 10:09:59 -05:00
}
}
assert ( found , "Payment account not found after adding" ) ;
2024-07-16 16:19:29 -04:00
// delete payment account
await user1 . deletePaymentAccount ( fiatAccount . getId ( ) ) ;
// no longer has payment account
try {
await user1 . getPaymentAccount ( fiatAccount . getId ( ) ) ;
throw new Error ( "Should have thrown error getting deleted payment account" ) ;
} catch ( err : any ) {
if ( err . message . indexOf ( "Should have thrown" ) >= 0 ) throw err ;
}
2022-02-16 10:09:59 -05:00
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can create crypto payment accounts (CI)" , async ( ) = > {
2022-02-16 10:09:59 -05:00
// test each crypto
2022-05-01 13:30:11 -04:00
for ( const testAccount of TestConfig . cryptoAddresses ) {
2022-12-13 08:44:20 +00:00
2021-10-22 13:51:57 -04:00
// create payment account
2023-10-02 08:16:54 -04:00
const name = testAccount . currencyCode + " " + testAccount . address . substr ( 0 , 8 ) + "... " + moneroTs . GenUtils . getUUID ( ) ;
2022-07-15 10:09:56 -04:00
const paymentAccount : PaymentAccount = await user1 . createCryptoPaymentAccount ( name , testAccount . currencyCode , testAccount . address ) ;
2021-10-22 13:51:57 -04:00
testCryptoPaymentAccount ( paymentAccount ) ;
testCryptoPaymentAccountEquals ( paymentAccount , testAccount , name ) ;
2022-12-13 08:44:20 +00:00
2021-10-22 13:51:57 -04:00
// fetch and test payment account
2022-10-14 16:09:10 -04:00
let fetchedAccount : PaymentAccount | undefined ;
2022-07-15 10:09:56 -04:00
for ( const account of await user1 . getPaymentAccounts ( ) ) {
2021-10-22 13:51:57 -04:00
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 ) ;
2024-07-16 16:19:29 -04:00
// delete payment account
await user1 . deletePaymentAccount ( paymentAccount . getId ( ) ) ;
// no longer has payment account
try {
await user1 . getPaymentAccount ( paymentAccount . getId ( ) ) ;
throw new Error ( "Should have thrown error getting deleted payment account" ) ;
} catch ( err : any ) {
if ( err . message . indexOf ( "Should have thrown" ) >= 0 ) throw err ;
}
2021-10-22 13:51:57 -04:00
}
2022-12-13 08:44:20 +00:00
2022-11-15 12:44:26 +01:00
// test invalid currency code
2023-02-11 11:59:41 +01:00
await expect ( async ( ) = > { await user1 . createCryptoPaymentAccount ( "My first account" , "ABC" , "123" ) ; } )
2022-04-11 17:52:21 -04:00
. rejects
2022-11-15 12:44:26 +01:00
. toThrow ( "crypto currency with code 'abc' not found" ) ;
2022-12-13 08:44:20 +00:00
2022-04-11 17:52:21 -04:00
// test invalid address
2023-02-11 11:59:41 +01:00
await expect ( async ( ) = > { await user1 . createCryptoPaymentAccount ( "My second account" , "ETH" , "123" ) ; } )
2022-04-11 17:52:21 -04:00
. rejects
2022-11-15 12:44:26 +01:00
. toThrow ( '123 is not a valid eth address' ) ;
2022-12-13 08:44:20 +00:00
2023-02-11 11:59:41 +01:00
// test address duplicity
2023-10-02 08:16:54 -04:00
let uid = "Unique account name " + moneroTs . GenUtils . getUUID ( ) ;
2023-04-06 07:42:21 -04:00
await user1 . createCryptoPaymentAccount ( uid , TestConfig . cryptoAddresses [ 0 ] . currencyCode , TestConfig . cryptoAddresses [ 0 ] . address )
await expect ( async ( ) = > { await user1 . createCryptoPaymentAccount ( uid , TestConfig . cryptoAddresses [ 0 ] . currencyCode , TestConfig . cryptoAddresses [ 0 ] . address ) ; } )
2023-02-11 11:59:41 +01:00
. rejects
2023-04-06 07:42:21 -04:00
. toThrow ( "Account '" + uid + "' is already taken" ) ;
2022-12-13 08:44:20 +00:00
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
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can prepare for trading (CI)" , async ( ) = > {
2022-12-15 21:39:43 +00:00
await prepareForTrading ( 5 , user1 , user2 ) ;
2022-05-26 10:58:15 -04:00
} ) ;
2022-05-11 13:17:41 -04:00
2022-12-23 09:32:42 +00:00
test ( "Can post and remove an offer (CI, sanity check)" , async ( ) = > {
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// wait for user1 to have unlocked balance to post offer
2023-12-27 16:58:30 -05:00
await waitForAvailableBalance ( 250000000000 n * 2 n , user1 ) ;
2022-12-13 08:44:20 +00:00
2021-09-17 09:33:58 -04:00
// get unlocked balance before reserving funds for offer
2022-08-17 12:22:08 -04:00
const availableBalanceBefore = BigInt ( ( await user1 . getBalances ( ) ) . getAvailableBalance ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-05-11 13:17:41 -04:00
// post crypto offer
2022-05-26 10:58:15 -04:00
let assetCode = "BCH" ;
2022-05-11 13:17:41 -04:00
let price = 1 / 17 ;
price = 1 / price ; // TODO: price in crypto offer is inverted
2023-10-28 10:20:56 -04:00
let offer : OfferInfo = await makeOffer ( { maker : { havenod : user1 } , assetCode : assetCode , price : price } ) ;
2021-12-14 13:04:02 -05:00
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2022-05-11 13:17:41 -04:00
assert . equal ( offer . getBaseCurrencyCode ( ) , assetCode ) ; // TODO: base and counter currencies inverted in crypto offer
assert . equal ( offer . getCounterCurrencyCode ( ) , "XMR" ) ;
2022-05-26 10:58:15 -04:00
assert . equal ( parseFloat ( offer . getPrice ( ) ) , price ) ;
2022-12-13 08:44:20 +00:00
2022-05-11 13:17:41 -04:00
// has offer
2022-07-15 10:09:56 -04:00
offer = await user1 . getMyOffer ( offer . getId ( ) ) ;
2022-05-11 13:17:41 -04:00
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2022-12-13 08:44:20 +00:00
2022-05-26 10:58:15 -04:00
// peer sees offer
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
2022-10-01 07:47:46 -04:00
if ( ! getOffer ( await user2 . getOffers ( assetCode , TestConfig . trade . direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in peer's offers after posted" ) ;
2022-12-13 08:44:20 +00:00
2022-05-11 13:17:41 -04:00
// cancel offer
2022-07-15 10:09:56 -04:00
await user1 . removeOffer ( offer . getId ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-05-11 13:17:41 -04:00
// offer is removed from my offers
2023-11-09 17:17:14 -05:00
if ( getOffer ( await user1 . getMyOffers ( assetCode , OfferDirection . BUY ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in my offers after removal" ) ;
2022-12-13 08:44:20 +00:00
2024-07-11 12:04:31 -04:00
// peer does not see offer
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
if ( getOffer ( await user2 . getOffers ( assetCode , TestConfig . trade . direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in peer's offers after removed" ) ;
2022-05-11 13:17:41 -04:00
// reserved balance released
2022-08-17 12:22:08 -04:00
expect ( BigInt ( ( await user1 . getBalances ( ) ) . getAvailableBalance ( ) ) ) . toEqual ( availableBalanceBefore ) ;
2022-12-13 08:44:20 +00:00
// post fiat offer
2022-05-11 13:17:41 -04:00
assetCode = "USD" ;
price = 180.0 ;
2023-10-28 10:20:56 -04:00
offer = await makeOffer ( { maker : { havenod : user1 } , assetCode : assetCode , price : price } ) ;
2022-05-11 13:17:41 -04:00
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
assert . equal ( offer . getBaseCurrencyCode ( ) , "XMR" ) ;
assert . equal ( offer . getCounterCurrencyCode ( ) , "USD" ) ;
2022-05-26 10:58:15 -04:00
assert . equal ( parseFloat ( offer . getPrice ( ) ) , price ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// has offer
2022-07-15 10:09:56 -04:00
offer = await user1 . getMyOffer ( offer . getId ( ) ) ;
2021-12-14 13:04:02 -05:00
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2022-12-13 08:44:20 +00:00
2024-07-11 12:04:31 -04:00
// peer sees offer
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
if ( ! getOffer ( await user2 . getOffers ( assetCode , TestConfig . trade . direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in peer's offers after posted" ) ;
2021-09-17 09:33:58 -04:00
// cancel offer
2022-07-15 10:09:56 -04:00
await user1 . removeOffer ( offer . getId ( ) ) ;
2022-12-13 08:44:20 +00:00
2021-09-17 09:33:58 -04:00
// offer is removed from my offers
2023-11-09 17:17:14 -05:00
if ( getOffer ( await user1 . getMyOffers ( assetCode , OfferDirection . BUY ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in my offers after removal" ) ;
2022-12-13 08:44:20 +00:00
2021-09-17 09:33:58 -04:00
// reserved balance released
2022-08-17 12:22:08 -04:00
expect ( BigInt ( ( await user1 . getBalances ( ) ) . getAvailableBalance ( ) ) ) . toEqual ( availableBalanceBefore ) ;
2024-07-11 12:04:31 -04:00
// peer does not see offer
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
if ( getOffer ( await user2 . getOffers ( assetCode , TestConfig . trade . direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in peer's offers after removed" ) ;
2022-04-08 18:33:37 -04:00
} ) ;
2022-05-14 19:35:02 -04:00
// TODO: provide number of confirmations in offer status
2022-12-13 08:44:20 +00:00
test ( "Can schedule offers with locked funds (CI)" , async ( ) = > {
2022-10-14 16:09:10 -04:00
let user3 : HavenoClient | undefined ;
2022-05-14 19:35:02 -04:00
let err : any ;
try {
2022-12-13 08:44:20 +00:00
2023-02-05 17:33:46 -05:00
// configure test
const completeTrade = true ;
const resolveDispute = Math . random ( ) < 0.5 ;
2022-07-15 10:09:56 -04:00
// start user3
user3 = await initHaveno ( ) ;
2023-10-02 08:16:54 -04:00
const user3Wallet = await moneroTs . connectToWalletRpc ( "http://127.0.0.1:" + user3 . getWalletRpcPort ( ) , TestConfig . defaultHavenod . walletUsername , TestConfig . defaultHavenod . accountPassword ) ;
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// fund user3 with 2 outputs of 0.5 XMR
2023-12-27 16:58:30 -05:00
const outputAmt = 500000000000 n ;
2022-07-15 10:09:56 -04:00
await fundOutputs ( [ user3Wallet ] , outputAmt , 2 , false ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// schedule offer
2022-05-26 10:58:15 -04:00
const assetCode = "BCH" ;
2023-11-09 17:17:14 -05:00
const direction = OfferDirection . BUY ;
2023-12-27 16:58:30 -05:00
const ctx = new TradeContext ( { maker : { havenod : user3 } , assetCode : assetCode , direction : direction , awaitFundsToMakeOffer : false , reserveExactAmount : true } ) ;
2023-02-05 17:33:46 -05:00
let offer : OfferInfo = await makeOffer ( ctx ) ;
2024-07-17 16:54:15 -04:00
assert . equal ( offer . getState ( ) , "PENDING" ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// has offer
2022-07-15 10:09:56 -04:00
offer = await user3 . getMyOffer ( offer . getId ( ) ) ;
2024-07-17 16:54:15 -04:00
assert . equal ( offer . getState ( ) , "PENDING" ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// balances unchanged
2023-12-27 16:58:30 -05:00
expect ( BigInt ( ( await user3 . getBalances ( ) ) . getPendingBalance ( ) ) ) . toEqual ( outputAmt * 2 n ) ;
expect ( BigInt ( ( await user3 . getBalances ( ) ) . getReservedOfferBalance ( ) ) ) . toEqual ( 0 n ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// peer does not see offer because it's scheduled
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
2022-07-15 10:09:56 -04:00
if ( getOffer ( await user1 . getOffers ( assetCode , direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in peer's offers before posted" ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// cancel offer
2022-07-15 10:09:56 -04:00
await user3 . removeOffer ( offer . getId ( ) ) ;
if ( getOffer ( await user3 . getOffers ( assetCode , direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found after canceling offer" ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// balances unchanged
2023-12-27 16:58:30 -05:00
expect ( BigInt ( ( await user3 . getBalances ( ) ) . getPendingBalance ( ) ) ) . toEqual ( outputAmt * 2 n ) ;
expect ( BigInt ( ( await user3 . getBalances ( ) ) . getReservedOfferBalance ( ) ) ) . toEqual ( 0 n ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// schedule offer
2023-12-27 16:58:30 -05:00
offer = await makeOffer ( { maker : { havenod : user3 } , assetCode : assetCode , direction : direction , awaitFundsToMakeOffer : false , reserveExactAmount : true } ) ;
2024-07-17 16:54:15 -04:00
assert . equal ( offer . getState ( ) , "PENDING" ) ;
2022-12-13 08:44:20 +00:00
2023-07-12 06:24:10 -04:00
// peer does not see offer because it's scheduled
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
if ( getOffer ( await user1 . getOffers ( assetCode , direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in peer's offers before posted" ) ;
// stop user3
2023-12-27 16:58:30 -05:00
let user3Config = { appName : user3.getAppName ( ) } ;
2022-07-15 10:09:56 -04:00
await releaseHavenoProcess ( user3 ) ;
2023-07-12 06:24:10 -04:00
// mine 10 blocks
await mineBlocks ( 10 ) ;
// restart user3
2022-07-15 10:09:56 -04:00
user3 = await initHaveno ( user3Config ) ;
2023-10-28 10:20:56 -04:00
ctx . maker . havenod = user3 ;
2022-12-13 08:44:20 +00:00
2023-12-27 16:58:30 -05:00
// awaiting split output
2023-07-12 06:24:10 -04:00
await waitForAvailableBalance ( outputAmt , user3 ) ;
2022-07-15 10:09:56 -04:00
offer = await user3 . getMyOffer ( offer . getId ( ) ) ;
2024-07-17 16:54:15 -04:00
assert . equal ( offer . getState ( ) , "PENDING" ) ;
2022-12-13 08:44:20 +00:00
2023-12-27 16:58:30 -05:00
// stop user3
user3Config = { appName : user3.getAppName ( ) } ;
await releaseHavenoProcess ( user3 ) ;
// mine 10 blocks
await mineBlocks ( 10 ) ;
// restart user3
user3 = await initHaveno ( user3Config ) ;
ctx . maker . havenod = user3 ;
// offer is available
await waitForAvailableBalance ( outputAmt + outputAmt / 2 n , user3 ) ;
await wait ( TestConfig . trade . walletSyncPeriodMs ) ;
offer = await user3 . getMyOffer ( offer . getId ( ) ) ;
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
ctx . maker . splitOutputTxFee = BigInt ( offer . getSplitOutputTxFee ( ) ) ;
// one output is reserved, remaining is unlocked
const balances = await user3 . getBalances ( ) ;
expect ( BigInt ( ( balances . getPendingBalance ( ) ) ) ) . toEqual ( 0 n ) ;
expect ( BigInt ( ( balances . getAvailableBalance ( ) ) ) ) . toBeGreaterThan ( outputAmt ) ; // TODO: testScheduleOffer(reserveExactAmount) to test these
expect ( BigInt ( ( balances . getReservedOfferBalance ( ) ) ) ) . toEqual ( outputAmt * 2 n - ctx . maker . splitOutputTxFee ! - BigInt ( balances . getAvailableBalance ( ) ) ) ;
2022-12-13 08:44:20 +00:00
2022-05-14 19:35:02 -04:00
// peer sees offer
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . maxTimePeerNoticeMs ) ;
2022-07-15 10:09:56 -04:00
if ( ! getOffer ( await user1 . getOffers ( assetCode , direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in peer's offers after posted" ) ;
2022-12-13 08:44:20 +00:00
2023-02-05 17:33:46 -05:00
// complete trade or cancel offer depending on configuration
if ( completeTrade ) {
HavenoUtils . log ( 1 , "Completing trade from scheduled offer, opening and resolving dispute: " + resolveDispute ) ;
await executeTrade ( Object . assign ( ctx , { buyerDisputeContext : resolveDispute ? DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK : DisputeContext.NONE } ) ) ;
} else {
2023-02-11 11:59:41 +01:00
2023-02-05 17:33:46 -05:00
// cancel offer
await user3 . removeOffer ( offer . getId ( ) ) ;
// offer is removed from my offers
if ( getOffer ( await user3 . getMyOffers ( assetCode ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in my offers after removal" ) ;
// reserved balance becomes unlocked
2023-12-27 16:58:30 -05:00
expect ( BigInt ( ( await user3 . getBalances ( ) ) . getAvailableBalance ( ) ) ) . toEqual ( outputAmt * 2 n ) ;
expect ( BigInt ( ( await user3 . getBalances ( ) ) . getPendingBalance ( ) ) ) . toEqual ( 0 n ) ;
expect ( BigInt ( ( await user3 . getBalances ( ) ) . getReservedOfferBalance ( ) ) ) . toEqual ( 0 n ) ;
2023-02-05 17:33:46 -05:00
}
2022-05-14 19:35:02 -04:00
} catch ( err2 ) {
err = err2 ;
}
// stop and delete instances
2022-07-15 10:09:56 -04:00
if ( user3 ) await releaseHavenoProcess ( user3 , true ) ;
2022-05-14 19:35:02 -04:00
if ( err ) throw err ;
} ) ;
2023-07-23 12:23:10 -04:00
test ( "Can reserve exact amount needed for offer (CI)" , async ( ) = > {
2023-06-10 10:02:15 -04:00
let randomOfferAmount = 1.0 + ( Math . random ( ) * 1.0 ) ; // random amount between 1 and 2 xmr
await executeTrade ( {
price : 150 ,
offerAmount : HavenoUtils.xmrToAtomicUnits ( randomOfferAmount ) ,
offerMinAmount : HavenoUtils.xmrToAtomicUnits ( . 15 ) ,
tradeAmount : HavenoUtils.xmrToAtomicUnits ( . 92 ) ,
2023-10-28 10:20:56 -04:00
reserveExactAmount : true ,
testBalanceChangeEndToEnd : true
2023-06-10 10:02:15 -04:00
} ) ;
} ) ;
2022-12-23 09:32:42 +00:00
test ( "Cannot post offer exceeding trade limit (CI, sanity check)" , async ( ) = > {
2023-08-31 09:15:04 -04:00
let assetCode = "USD" ;
2023-11-01 06:39:35 -04:00
const account = await createPaymentAccount ( user1 , assetCode , "zelle" ) ;
// test posting buy offer above limit
try {
await executeTrade ( {
2024-02-12 10:41:21 -05:00
offerAmount : moneroTs.MoneroUtils.xmrToAtomicUnits ( 3.1 ) ,
2023-11-09 17:17:14 -05:00
direction : OfferDirection.BUY ,
2023-11-01 06:39:35 -04:00
assetCode : assetCode ,
makerPaymentAccountId : account.getId ( ) ,
takeOffer : false
} ) ;
throw new Error ( "Should have rejected posting offer above trade limit" )
2024-08-29 11:32:14 -04:00
} catch ( err : any ) {
2023-11-01 06:39:35 -04:00
assert ( err . message . indexOf ( "amount is larger than" ) === 0 ) ;
}
// test posting sell offer above limit
2022-12-15 21:39:43 +00:00
try {
2023-11-01 06:39:35 -04:00
await executeTrade ( {
2024-02-12 10:41:21 -05:00
offerAmount : moneroTs.MoneroUtils.xmrToAtomicUnits ( 12.1 ) ,
2023-11-09 17:17:14 -05:00
direction : OfferDirection.SELL ,
2023-11-01 06:39:35 -04:00
assetCode : assetCode ,
makerPaymentAccountId : account.getId ( ) ,
takeOffer : false
} ) ;
2022-12-15 21:39:43 +00:00
throw new Error ( "Should have rejected posting offer above trade limit" )
2024-08-29 11:32:14 -04:00
} catch ( err : any ) {
2022-12-15 21:39:43 +00:00
assert ( err . message . indexOf ( "amount is larger than" ) === 0 ) ;
}
2023-11-01 06:39:35 -04:00
// test that sell limit is higher than buy limit
let offerId = await executeTrade ( {
2023-12-27 16:58:30 -05:00
offerAmount : 2100000000000n ,
2023-11-09 17:17:14 -05:00
direction : OfferDirection.SELL ,
2023-11-01 06:39:35 -04:00
assetCode : assetCode ,
makerPaymentAccountId : account.getId ( ) ,
takeOffer : false
} ) ;
await user1 . removeOffer ( offerId ) ;
2022-12-15 21:39:43 +00:00
} ) ;
2023-05-30 18:08:04 -04:00
test ( "Can complete a trade within a range" , async ( ) = > {
2023-08-31 09:15:04 -04:00
// create payment accounts
2023-09-09 10:25:52 -04:00
let paymentMethodId = "cash_at_atm" ;
let assetCode = "aud" ;
2023-11-13 10:41:17 -05:00
let makerPaymentAccount = await createPaymentAccount ( user1 , assetCode , paymentMethodId ) ; // TODO: support getPaymentAccount() which gets or creates
2023-08-31 09:15:04 -04:00
let takerPaymentAccount = await createPaymentAccount ( user2 , assetCode , paymentMethodId ) ;
2024-07-17 16:56:29 -04:00
// get trade statistics before
const tradeStatisticsPre = await arbitrator . getTradeStatistics ( ) ;
2023-08-31 09:15:04 -04:00
// execute trade
2024-04-07 08:13:09 -04:00
const offerAmount = HavenoUtils . xmrToAtomicUnits ( 2 ) ;
const offerMinAmount = HavenoUtils . xmrToAtomicUnits ( . 15 ) ;
const tradeAmount = getRandomBigIntWithinRange ( offerMinAmount , offerAmount ) ;
2024-07-17 16:56:29 -04:00
const ctx : Partial < TradeContext > = {
2023-09-09 10:25:52 -04:00
price : 142.23 ,
2024-04-07 08:13:09 -04:00
offerAmount : offerAmount ,
offerMinAmount : offerMinAmount ,
tradeAmount : tradeAmount ,
2023-07-27 09:57:40 -04:00
testPayoutUnlocked : true , // override to test unlock
2023-08-31 09:15:04 -04:00
makerPaymentAccountId : makerPaymentAccount.getId ( ) ,
takerPaymentAccountId : takerPaymentAccount.getId ( ) ,
2023-10-28 10:20:56 -04:00
assetCode : assetCode ,
testBalanceChangeEndToEnd : true
2024-07-17 16:56:29 -04:00
}
await executeTrade ( ctx ) ;
// test trade statistics after
if ( ctx . buyerSendsPayment && ctx . sellerReceivesPayment ) {
const tradeStatisticsPost = await arbitrator . getTradeStatistics ( ) ;
assert ( tradeStatisticsPost . length - tradeStatisticsPre . length === 1 ) ;
}
2022-10-01 07:47:46 -04:00
} ) ;
2022-03-09 04:43:30 -08:00
2022-12-23 09:32:42 +00:00
test ( "Can complete trades at the same time (CI, sanity check)" , async ( ) = > {
2023-11-13 10:41:17 -05:00
2024-04-07 08:13:09 -04:00
// create trade contexts with customized payment methods and random amounts
2023-03-06 13:27:52 -05:00
const ctxs = getTradeContexts ( TestConfig . assetCodes . length ) ;
2023-11-13 10:41:17 -05:00
for ( let i = 0 ; i < ctxs . length ; i ++ ) {
ctxs [ i ] . assetCode = TestConfig . assetCodes [ i ] ; // test each asset code
2024-04-07 08:13:09 -04:00
ctxs [ i ] . offerAmount = getRandomBigIntWithinPercent ( TestConfig . trade . offerAmount ! , 0.15 ) ;
2023-11-13 10:41:17 -05:00
let paymentMethodId ;
if ( ctxs [ i ] . assetCode === "USD" ) paymentMethodId = "zelle" ;
if ( ctxs [ i ] . assetCode === "EUR" ) paymentMethodId = "revolut" ;
ctxs [ i ] . makerPaymentAccountId = ( await createPaymentAccount ( ctxs [ i ] . maker . havenod ! , ctxs [ i ] . assetCode ! , paymentMethodId ) ) . getId ( ) ;
ctxs [ i ] . takerPaymentAccountId = ( await createPaymentAccount ( ctxs [ i ] . taker . havenod ! , ctxs [ i ] . assetCode ! , paymentMethodId ) ) . getId ( ) ;
}
// execute trades with capped concurrency for CI tests
2024-04-08 07:27:03 -04:00
await executeTrades ( ctxs ) ;
2022-11-04 15:57:42 -04:00
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can complete all trade combinations (stress)" , async ( ) = > {
2022-11-04 15:57:42 -04:00
// generate trade context for each combination (buyer/seller, maker/taker, dispute(s), dispute winner)
2024-06-03 10:41:48 -04:00
let ctxs : TradeContext [ ] = [ ] ;
2022-11-04 15:57:42 -04:00
const MAKER_OPTS = [ TradeRole . MAKER , TradeRole . TAKER ] ;
2023-11-09 17:17:14 -05:00
const DIRECTION_OPTS = [ OfferDirection . BUY , OfferDirection . SELL ] ;
2022-11-04 15:57:42 -04:00
const BUYER_DISPUTE_OPTS = [ DisputeContext . NONE , DisputeContext . OPEN_AFTER_DEPOSITS_UNLOCK , DisputeContext . OPEN_AFTER_PAYMENT_SENT ] ;
const SELLER_DISPUTE_OPTS = [ DisputeContext . NONE , DisputeContext . OPEN_AFTER_DEPOSITS_UNLOCK , DisputeContext . OPEN_AFTER_PAYMENT_SENT ] ;
const DISPUTE_WINNER_OPTS = [ DisputeResult . Winner . BUYER , DisputeResult . Winner . SELLER ] ;
for ( let i = 0 ; i < MAKER_OPTS . length ; i ++ ) {
for ( let j = 0 ; j < DIRECTION_OPTS . length ; j ++ ) {
for ( let k = 0 ; k < BUYER_DISPUTE_OPTS . length ; k ++ ) {
for ( let l = 0 ; l < SELLER_DISPUTE_OPTS . length ; l ++ ) {
for ( let m = 0 ; m < DISPUTE_WINNER_OPTS . length ; m ++ ) {
if ( BUYER_DISPUTE_OPTS [ k ] !== DisputeContext . NONE && SELLER_DISPUTE_OPTS [ l ] !== DisputeContext . NONE ) continue ; // skip both opening a dispute
2023-10-28 10:20:56 -04:00
const ctx : Partial < TradeContext > = {
2022-12-21 15:25:52 +00:00
walletSyncPeriodMs : 8000 , // increase for stress test
maxTimePeerNoticeMs : 8000 ,
2023-10-28 10:20:56 -04:00
maker : { havenod : MAKER_OPTS [ i ] === TradeRole . MAKER ? user1 : user2 } ,
taker : { havenod : MAKER_OPTS [ i ] === TradeRole . MAKER ? user2 : user1 } ,
2022-11-04 15:57:42 -04:00
direction : DIRECTION_OPTS [ j ] ,
buyerDisputeContext : BUYER_DISPUTE_OPTS [ k ] ,
sellerDisputeContext : SELLER_DISPUTE_OPTS [ l ] ,
disputeWinner : DISPUTE_WINNER_OPTS [ m ] ,
2024-04-07 08:13:09 -04:00
disputeSummary : "After much deliberation, " + ( DISPUTE_WINNER_OPTS [ m ] === DisputeResult . Winner . BUYER ? "buyer" : "seller" ) + " is winner" ,
offerAmount : getRandomBigIntWithinPercent ( TestConfig . trade . offerAmount ! , 0.15 )
2022-11-04 15:57:42 -04:00
} ;
2023-10-28 10:20:56 -04:00
ctxs . push ( Object . assign ( { } , new TradeContext ( TestConfig . trade ) , ctx ) ) ;
2022-11-04 15:57:42 -04:00
}
}
}
}
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// execute trades
2024-06-03 10:41:48 -04:00
const ctxIdx = undefined ; // run single index for debugging
if ( ctxIdx !== undefined ) ctxs = ctxs . slice ( ctxIdx , ctxIdx + 1 ) ;
2022-11-04 15:57:42 -04:00
HavenoUtils . log ( 0 , "Executing " + ctxs . length + " trade configurations" ) ;
2022-11-28 13:15:04 +00:00
await executeTrades ( ctxs ) ;
2022-02-16 10:09:59 -05:00
} ) ;
2022-12-23 09:32:42 +00:00
test ( "Can go offline while completing a trade (CI, sanity check)" , async ( ) = > {
2022-08-14 19:20:08 -04:00
let traders : HavenoClient [ ] = [ ] ;
2023-10-28 10:20:56 -04:00
let ctx : TradeContext = new TradeContext ( TestConfig . trade ) ;
2022-08-14 19:20:08 -04:00
let err : any ;
try {
2022-12-13 08:44:20 +00:00
2023-04-05 08:42:55 -04:00
// start 2 trader processes
2022-08-14 19:20:08 -04:00
HavenoUtils . log ( 1 , "Starting trader processes" ) ;
traders = await initHavenos ( 2 ) ;
2022-12-13 08:44:20 +00:00
2024-07-17 16:54:15 -04:00
// fund traders
HavenoUtils . log ( 1 , "Funding traders" ) ;
const tradeAmount = 250000000000 n ;
await waitForAvailableBalance ( tradeAmount * 2 n , . . . traders ) ;
2022-10-01 07:47:46 -04:00
// create trade config
2023-10-28 10:20:56 -04:00
ctx . maker . havenod = traders [ 0 ] ;
ctx . taker . havenod = traders [ 1 ] ;
2022-11-04 15:57:42 -04:00
ctx . buyerOfflineAfterTake = true ;
ctx . sellerOfflineAfterTake = true ;
ctx . buyerOfflineAfterPaymentSent = true ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// execute trade
2022-11-04 15:57:42 -04:00
await executeTrade ( ctx ) ;
2022-08-14 19:20:08 -04:00
} catch ( e ) {
err = e ;
}
2022-12-13 08:44:20 +00:00
2022-08-14 19:20:08 -04:00
// stop traders
2023-10-28 10:20:56 -04:00
if ( ctx . maker . havenod ) await releaseHavenoProcess ( ctx . maker . havenod , true ) ;
if ( ctx . taker . havenod ) await releaseHavenoProcess ( ctx . taker . havenod , true ) ;
2022-08-14 19:20:08 -04:00
if ( err ) throw err ;
} ) ;
2023-10-28 10:20:56 -04:00
test ( "Can resolve a dispute (CI)" , async ( ) = > {
2023-01-23 16:17:00 -05:00
2023-10-28 10:20:56 -04:00
// create payment accounts
let paymentMethodId = "revolut" ;
let assetCode = "usd" ;
let makerPaymentAccount = await createPaymentAccount ( user1 , assetCode , paymentMethodId ) ;
let takerPaymentAccount = await createPaymentAccount ( user2 , assetCode , paymentMethodId ) ;
2023-04-05 08:42:55 -04:00
2023-10-28 10:20:56 -04:00
// execute trade
await executeTrade ( {
price : 142.23 ,
offerAmount : HavenoUtils.xmrToAtomicUnits ( 2 ) ,
offerMinAmount : HavenoUtils.xmrToAtomicUnits ( . 15 ) ,
tradeAmount : HavenoUtils.xmrToAtomicUnits ( 1 ) ,
testPayoutUnlocked : true , // override to test unlock
makerPaymentAccountId : makerPaymentAccount.getId ( ) ,
takerPaymentAccountId : takerPaymentAccount.getId ( ) ,
assetCode : assetCode ,
buyerDisputeContext : DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK ,
disputeWinner : DisputeResult.Winner.SELLER ,
2023-11-13 09:33:34 -05:00
disputeWinnerAmount : HavenoUtils.xmrToAtomicUnits ( . 767 ) ,
2023-10-28 10:20:56 -04:00
disputeReason : DisputeResult.Reason.OTHER ,
disputeSummary : "Payment not completed, so returning trade amount to seller." ,
testBalanceChangeEndToEnd : true
} ) ;
2023-01-23 16:17:00 -05:00
2023-10-28 10:20:56 -04:00
// TODO: test receiver = BUYER
2023-01-23 16:17:00 -05:00
} ) ;
2022-12-13 08:44:20 +00:00
test ( "Can resolve disputes (CI)" , async ( ) = > {
2023-10-28 10:20:56 -04:00
// execute all configs unless config index given
let configIdx = undefined ;
let testBalancesSequentially = false ; // runs each config sequentially to test balances before and after // TODO: this test takes much longer to run in sequence in order to test balances. use test weight config
2022-12-13 08:44:20 +00:00
2023-10-28 10:20:56 -04:00
// create test configurations which stop before payment sent
2022-11-23 09:41:48 +00:00
const ctxs = getTradeContexts ( 4 ) ;
2022-11-04 15:57:42 -04:00
for ( const config of ctxs ) config . buyerSendsPayment = false ;
2023-10-28 10:20:56 -04:00
Object . assign ( ctxs [ 3 ] , {
offerAmount : HavenoUtils.xmrToAtomicUnits ( 1 ) ,
offerMinAmount : HavenoUtils.xmrToAtomicUnits ( . 15 ) ,
tradeAmount : HavenoUtils.xmrToAtomicUnits ( . 578 ) ,
} ) ;
// initiate trades
const tradeIds = await executeTrades ( ctxs . slice ( configIdx , configIdx === undefined ? undefined : configIdx + 1 ) ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// open disputes at same time but do not resolve
2023-10-28 10:20:56 -04:00
const trade1 = await user1 . getTrade ( tradeIds [ configIdx === undefined ? 1 : 0 ] ) ;
const trade2 = await user1 . getTrade ( tradeIds [ configIdx === undefined ? 2 : 0 ] ) ;
2022-11-04 15:57:42 -04:00
Object . assign ( ctxs [ 0 ] , {
2022-10-01 07:47:46 -04:00
resolveDispute : false ,
2022-11-04 15:57:42 -04:00
sellerDisputeContext : DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK ,
2022-10-01 07:47:46 -04:00
disputeWinner : DisputeResult.Winner.SELLER ,
disputeReason : DisputeResult.Reason.PEER_WAS_LATE ,
disputeSummary : "Seller is winner"
2022-10-26 01:05:49 -04:00
} ) ;
2022-11-04 15:57:42 -04:00
Object . assign ( ctxs [ 1 ] , {
2022-10-01 07:47:46 -04:00
resolveDispute : false ,
2022-11-04 15:57:42 -04:00
buyerDisputeContext : DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK ,
2022-10-01 07:47:46 -04:00
disputeWinner : DisputeResult.Winner.BUYER ,
disputeReason : DisputeResult.Reason.SELLER_NOT_RESPONDING ,
disputeSummary : "Split trade amount" ,
2023-12-27 16:58:30 -05:00
disputeWinnerAmount : BigInt ( trade1 . getAmount ( ) ) / 2 n + BigInt ( trade1 . getBuyerSecurityDeposit ( ) )
2022-10-26 01:05:49 -04:00
} ) ;
2022-11-23 09:41:48 +00:00
Object . assign ( ctxs [ 2 ] , {
2022-10-01 07:47:46 -04:00
resolveDispute : false ,
2022-11-04 15:57:42 -04:00
buyerDisputeContext : DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK ,
2022-10-01 07:47:46 -04:00
disputeWinner : DisputeResult.Winner.SELLER ,
disputeReason : DisputeResult.Reason.TRADE_ALREADY_SETTLED ,
disputeSummary : "Seller gets everything" ,
2023-03-07 13:20:02 -05:00
disputeWinnerAmount : BigInt ( trade2 . getAmount ( ) ) + BigInt ( trade2 . getBuyerSecurityDeposit ( ) ) + BigInt ( trade2 . getSellerSecurityDeposit ( ) )
2022-10-26 01:05:49 -04:00
} ) ;
2022-11-23 09:41:48 +00:00
Object . assign ( ctxs [ 3 ] , {
2022-11-04 15:57:42 -04:00
resolveDispute : false ,
buyerSendsPayment : true ,
sellerDisputeContext : DisputeContext.OPEN_AFTER_PAYMENT_SENT ,
disputeWinner : DisputeResult.Winner.BUYER ,
disputeReason : DisputeResult.Reason.TRADE_ALREADY_SETTLED ,
disputeSummary : "Buyer wins dispute after sending payment" ,
2023-11-13 09:33:34 -05:00
disputeWinnerAmount : HavenoUtils.xmrToAtomicUnits ( . 1171 ) ,
2022-11-04 15:57:42 -04:00
} ) ;
HavenoUtils . log ( 1 , "Opening disputes" ) ;
2023-10-28 10:20:56 -04:00
await executeTrades ( ctxs . slice ( configIdx , configIdx === undefined ? undefined : configIdx + 1 ) ) ;
2022-12-13 08:44:20 +00:00
2022-10-14 16:09:10 -04:00
// resolve disputes
2023-10-28 10:20:56 -04:00
for ( const ctx of ctxs ) {
ctx . resolveDispute = true ;
ctx . testPayoutUnlocked = false ;
}
2022-10-14 16:09:10 -04:00
HavenoUtils . log ( 1 , "Resolving disputes" ) ;
2023-10-28 10:20:56 -04:00
await executeTrades ( ctxs . slice ( configIdx , configIdx === undefined ? undefined : configIdx + 1 ) , { concurrentTrades : ! testBalancesSequentially } ) ;
} ) ;
2023-12-07 18:05:15 -05:00
test ( "Can go offline while resolving a dispute (CI)" , async ( ) = > {
2023-10-28 10:20:56 -04:00
let traders : HavenoClient [ ] = [ ] ;
let ctx : Partial < TradeContext > = { } ;
let err : any ;
try {
// start trader processes
HavenoUtils . log ( 1 , "Starting trader processes" ) ;
traders = await initHavenos ( 2 ) ;
// create trade config
ctx = new TradeContext ( {
maker : { havenod : traders [ 0 ] } ,
taker : { havenod : traders [ 1 ] } ,
buyerOfflineAfterTake : true ,
sellerOfflineAfterDisputeOpened : true ,
buyerOfflineAfterDisputeOpened : false ,
sellerDisputeContext : DisputeContext.OPEN_AFTER_DEPOSITS_UNLOCK ,
disputeWinner : DisputeResult.Winner.SELLER ,
disputeReason : DisputeResult.Reason.NO_REPLY ,
disputeSummary : "Seller wins dispute because buyer has not replied" ,
testPayoutUnlocked : false
} ) ;
// fund traders
2023-12-27 16:58:30 -05:00
const tradeAmount = 250000000000 n ;
await waitForAvailableBalance ( tradeAmount * 2 n , . . . traders ) ;
2023-10-28 10:20:56 -04:00
// execute trade
await executeTrade ( ctx ) ;
} catch ( e ) {
err = e ;
}
// stop and delete traders
2024-01-26 09:11:12 -05:00
if ( ctx . maker && ctx . maker . havenod ) await releaseHavenoProcess ( ctx . maker ! . havenod ! , true ) ;
if ( ctx . taker && ctx . taker . havenod ) await releaseHavenoProcess ( ctx . taker ! . havenod ! , true ) ; // closing this client after first induces HttpClientImpl.shutdown() to hang, so this tests timeout handling
2023-10-28 10:20:56 -04:00
if ( ctx . sellerAppName ) deleteHavenoInstanceByAppName ( ctx . sellerAppName ! ) ; // seller is offline
if ( err ) throw err ;
2022-03-07 09:57:00 -08:00
} ) ;
2022-12-23 09:32:42 +00:00
test ( "Cannot make or take offer with insufficient unlocked funds (CI, sanity check)" , async ( ) = > {
2022-10-14 16:09:10 -04:00
let user3 : HavenoClient | undefined ;
2022-02-16 10:09:59 -05:00
let err : any ;
try {
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// start user3
user3 = await initHaveno ( ) ;
2022-12-13 08:44:20 +00:00
2023-08-31 09:15:04 -04:00
// user3 creates payment account
2023-10-28 10:20:56 -04:00
const paymentAccount = await createPaymentAccount ( user3 , TestConfig . trade . assetCode ! ) ;
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// user3 cannot make offer with insufficient funds
2022-02-16 10:09:59 -05:00
try {
2023-10-28 10:20:56 -04:00
await makeOffer ( { maker : { havenod : user3 } , makerPaymentAccountId : paymentAccount.getId ( ) , awaitFundsToMakeOffer : false } ) ;
2022-02-16 10:09:59 -05:00
throw new Error ( "Should have failed making offer with insufficient funds" )
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-10-01 07:47:46 -04:00
if ( ! err . message . includes ( "not enough money" ) ) throw err ;
2022-05-31 15:45:17 -04:00
const errTyped = err as HavenoError ;
2022-02-16 10:09:59 -05:00
assert . equal ( errTyped . code , 2 ) ;
}
2022-12-13 08:44:20 +00:00
2023-02-24 11:58:33 -05:00
// user1 gets or posts offer
2023-01-10 08:30:47 -05:00
const offers : OfferInfo [ ] = await user1 . getMyOffers ( TestConfig . trade . assetCode ) ;
2022-02-16 10:09:59 -05:00
let offer : OfferInfo ;
if ( offers . length ) offer = offers [ 0 ] ;
else {
2023-12-27 16:58:30 -05:00
const tradeAmount = 250000000000 n ;
await waitForAvailableBalance ( tradeAmount * 2 n , user1 ) ;
2023-10-28 10:20:56 -04:00
offer = await makeOffer ( { maker : { havenod : user1 } , offerAmount : tradeAmount , awaitFundsToMakeOffer : false } ) ;
2022-02-16 10:09:59 -05:00
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ;
2022-02-16 10:09:59 -05:00
}
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// user3 cannot take offer with insufficient funds
2022-02-16 10:09:59 -05:00
try {
2022-07-15 10:09:56 -04:00
await user3 . takeOffer ( offer . getId ( ) , paymentAccount . getId ( ) ) ;
2022-02-16 10:09:59 -05:00
throw new Error ( "Should have failed taking offer with insufficient funds" )
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-05-31 15:45:17 -04:00
const errTyped = err as HavenoError ;
2022-02-16 10:09:59 -05:00
assert ( errTyped . message . includes ( "not enough money" ) , "Unexpected error: " + errTyped . message ) ;
assert . equal ( errTyped . code , 2 ) ;
}
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// user3 does not have trade
2022-02-16 10:09:59 -05:00
try {
2022-07-15 10:09:56 -04:00
await user3 . getTrade ( offer . getId ( ) ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-05-31 15:45:17 -04:00
const errTyped = err as HavenoError ;
2022-02-16 10:09:59 -05:00
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
}
2023-02-24 11:58:33 -05:00
// remove offer if posted
if ( ! offers . length ) await user1 . removeOffer ( offer . getId ( ) ) ;
2022-02-16 10:09:59 -05:00
} catch ( err2 ) {
err = err2 ;
}
2022-12-13 08:44:20 +00:00
2022-07-15 10:09:56 -04:00
// stop user3
if ( user3 ) await releaseHavenoProcess ( user3 , true ) ;
2022-02-16 10:09:59 -05:00
if ( err ) throw err ;
} ) ;
2022-12-16 17:33:16 +00:00
test ( "Invalidates offers when reserved funds are spent (CI)" , async ( ) = > {
2021-12-14 13:04:02 -05:00
let err ;
let tx ;
try {
2023-07-12 06:24:10 -04:00
2022-07-15 10:09:56 -04:00
// wait for user1 to have unlocked balance for trade
2023-12-27 16:58:30 -05:00
const tradeAmount = 250000000000 n ;
await waitForAvailableBalance ( tradeAmount * 2 n , user1 ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// get frozen key images before posting offer
2022-10-14 16:09:10 -04:00
const frozenKeyImagesBefore : any [ ] = [ ] ;
2022-07-15 10:09:56 -04:00
for ( const frozenOutput of await user1Wallet . getOutputs ( { isFrozen : true } ) ) frozenKeyImagesBefore . push ( frozenOutput . getKeyImage ( ) . getHex ( ) ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// post offer
await wait ( 1000 ) ;
2022-05-01 13:30:11 -04:00
const assetCode = getRandomAssetCode ( ) ;
2023-10-28 10:20:56 -04:00
const offer : OfferInfo = await makeOffer ( { maker : { havenod : user1 } , assetCode : assetCode , offerAmount : tradeAmount } ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// get key images reserved by offer
2022-10-14 16:09:10 -04:00
const reservedKeyImages : any [ ] = [ ] ;
const frozenKeyImagesAfter : any [ ] = [ ] ;
2022-07-15 10:09:56 -04:00
for ( const frozenOutput of await user1Wallet . getOutputs ( { isFrozen : true } ) ) frozenKeyImagesAfter . push ( frozenOutput . getKeyImage ( ) . getHex ( ) ) ;
2022-05-01 13:30:11 -04:00
for ( const frozenKeyImageAfter of frozenKeyImagesAfter ) {
2021-12-14 13:04:02 -05:00
if ( ! frozenKeyImagesBefore . includes ( frozenKeyImageAfter ) ) reservedKeyImages . push ( frozenKeyImageAfter ) ;
}
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// offer is available to peers
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ;
2023-11-09 17:17:14 -05:00
if ( ! getOffer ( await user2 . getOffers ( assetCode , OfferDirection . BUY ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in peer's offers after posting" ) ;
2022-12-13 08:44:20 +00:00
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" ) ;
2022-07-15 10:09:56 -04:00
await user1Wallet . thawOutput ( reservedKeyImages [ 0 ] ) ;
tx = await user1Wallet . sweepOutput ( { keyImage : reservedKeyImages [ 0 ] , address : await user1Wallet . getPrimaryAddress ( ) , relay : false } ) ;
2024-08-29 11:32:14 -04:00
await monerod . submitTxHex ( tx . getFullHex ( ) ! , true ) ;
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// mine block so spend is confirmed
await mineBlocks ( 1 ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// offer is removed from peer offers
2022-12-18 15:58:24 +00:00
await wait ( 20000 ) ;
2023-11-09 17:17:14 -05:00
if ( getOffer ( await user2 . getOffers ( assetCode , OfferDirection . BUY ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in peer's offers after reserved funds spent" ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// offer is removed from my offers
2023-11-09 17:17:14 -05:00
if ( getOffer ( await user1 . getMyOffers ( assetCode , OfferDirection . BUY ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was found in my offers after reserved funds spent" ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// offer is automatically cancelled
try {
2022-07-15 10:09:56 -04:00
await user1 . removeOffer ( offer . getId ( ) ) ;
2021-12-14 13:04:02 -05:00
throw new Error ( "cannot remove invalidated offer" ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
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
}
2022-12-13 08:44:20 +00: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)
2022-11-24 17:06:45 +00:00
test ( "Can handle 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 {
2022-12-13 08:44:20 +00:00
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" ) ;
2023-12-27 16:58:30 -05:00
const tradeAmount = 250000000000 n ;
await waitForAvailableBalance ( tradeAmount * 2 n , traders [ 0 ] , traders [ 1 ] , traders [ 2 ] ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// trader 0 posts offer
2022-02-16 10:09:59 -05:00
HavenoUtils . log ( 1 , "Posting offer" ) ;
2023-10-28 10:20:56 -04:00
let offer = await makeOffer ( { maker : { havenod : traders [ 0 ] } , offerAmount : tradeAmount } ) ;
2021-12-14 13:04:02 -05:00
offer = await traders [ 0 ] . getMyOffer ( offer . getId ( ) ) ;
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2022-12-13 08:44:20 +00:00
2022-01-08 05:21:32 -08:00
// wait for offer to be seen
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ;
2022-12-13 08:44:20 +00:00
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 {
2023-10-02 08:16:54 -04:00
const traderWallet = await moneroTs . connectToWalletRpc ( "http://localhost:" + traders [ 1 ] . getWalletRpcPort ( ) , TestConfig . defaultHavenod . walletUsername , TestConfig . defaultHavenod . accountPassword ) ;
2022-05-01 13:30:11 -04:00
for ( const 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 } ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2021-12-14 13:04:02 -05:00
console . log ( "Caught error sweeping funds!" ) ;
console . log ( err ) ;
}
} ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// 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" )
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
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
}
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// TODO: test it's unavailable right after taking (taker will know before maker)
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// trader 0's offer remains available
2024-04-24 21:19:25 -04:00
await wait ( 15000 ) ; // give time for trade initialization to fail and offer to become available
2021-12-14 13:04:02 -05:00
offer = await traders [ 0 ] . getMyOffer ( offer . getId ( ) ) ;
if ( offer . getState ( ) !== "AVAILABLE" ) {
2024-04-18 13:00:37 -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 . tradeStepTimeoutMs ) ; // wait for offer to become available after timeout
offer = await traders [ 0 ] . getMyOffer ( offer . getId ( ) ) ;
assert . equal ( offer . getState ( ) , "AVAILABLE" ) ;
2021-12-14 13:04:02 -05:00
}
2022-12-13 08:44:20 +00:00
2022-07-17 07:57:51 -04:00
// trader 0 spends trade funds after trader 2 takes offer
2021-12-14 13:04:02 -05:00
wait ( 3000 ) . then ( async function ( ) {
try {
2023-10-02 08:16:54 -04:00
const traderWallet = await moneroTs . connectToWalletRpc ( "http://localhost:" + traders [ 0 ] . getWalletRpcPort ( ) , TestConfig . defaultHavenod . walletUsername , TestConfig . defaultHavenod . accountPassword ) ;
2022-05-01 13:30:11 -04:00
for ( const 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 } ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2021-12-14 13:04:02 -05:00
console . log ( "Caught error sweeping funds!" ) ;
console . log ( err ) ;
}
} ) ;
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// 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" )
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2023-12-31 10:13:44 -05:00
// determine if error is expected
let expected = false ;
const expectedErrMsgs = [ "not enough unlocked money" , "timeout reached. protocol did not complete" , "trade is already taken" ] ;
for ( const expectedErrMsg of expectedErrMsgs ) {
if ( err . message . indexOf ( expectedErrMsg ) >= 0 ) {
expected = true ;
break ;
}
}
if ( ! expected ) throw err ;
2021-12-14 13:04:02 -05:00
}
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// trader 2's balance is unreserved
2022-05-01 13:30:11 -04:00
const trader2Balances = await traders [ 2 ] . getBalances ( ) ;
2023-12-27 16:58:30 -05:00
expect ( BigInt ( trader2Balances . getReservedTradeBalance ( ) ) ) . toEqual ( 0 n ) ;
expect ( BigInt ( trader2Balances . getAvailableBalance ( ) ) ) . toBeGreaterThan ( 0 n ) ;
2021-12-14 13:04:02 -05:00
} catch ( err2 ) {
err = err2 ;
2021-09-17 09:33:58 -04:00
}
2022-12-13 08:44:20 +00:00
2021-12-14 13:04:02 -05:00
// stop traders
2022-05-01 13:30:11 -04:00
for ( const trader of traders ) await releaseHavenoProcess ( trader , true ) ;
2021-12-14 13:04:02 -05:00
if ( err ) throw err ;
2021-09-17 09:33:58 -04:00
} ) ;
2022-08-12 08:28:58 -04:00
// TODO: test opening and resolving dispute as arbitrator and traders go offline
test ( "Selects arbitrators which are online, registered, and least used" , async ( ) = > {
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// complete 2 trades using main arbitrator so it's most used
// TODO: these trades are not registered with seednode until it's restarted
HavenoUtils . log ( 1 , "Preparing for trades" ) ;
await prepareForTrading ( 4 , user1 , user2 ) ;
HavenoUtils . log ( 1 , "Completing trades with main arbitrator" ) ;
2023-05-11 08:56:59 -04:00
await executeTrades ( getTradeContexts ( 2 ) , { testPayoutConfirmed : false } ) ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// start and register arbitrator2
let arbitrator2 = await initHaveno ( ) ;
HavenoUtils . log ( 1 , "Registering arbitrator2" ) ;
await arbitrator2 . registerDisputeAgent ( "arbitrator" , getArbitratorPrivKey ( 1 ) ) ; // TODO: re-registering with same address corrupts messages (Cannot decrypt) because existing pub key; overwrite? or throw when registration fails because dispute map can't be updated
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// get internal api addresses
2023-05-16 11:56:41 -04:00
const arbitrator1ApiUrl = "localhost:" + TestConfig . ports . get ( getPort ( arbitrator . getUrl ( ) ) ) ! [ 1 ] ; // TODO: havenod.getApiUrl()?
2022-12-03 13:03:44 +00:00
const arbitrator2ApiUrl = "localhost:" + TestConfig . ports . get ( getPort ( arbitrator2 . getUrl ( ) ) ) ! [ 1 ] ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
let err = undefined ;
try {
2022-12-13 08:44:20 +00:00
2023-05-16 11:56:41 -04:00
// post offers signed by each arbitrator randomly
HavenoUtils . log ( 1 , "Posting offers signed by both arbitrators randomly" ) ;
let offer1 : OfferInfo | undefined ;
let offer2 : OfferInfo | undefined ;
while ( true ) {
2023-10-28 10:20:56 -04:00
const offer = await makeOffer ( { maker : { havenod : user1 } } ) ;
2023-05-16 11:56:41 -04:00
if ( offer . getArbitratorSigner ( ) === arbitrator1ApiUrl && ! offer1 ) offer1 = offer ;
else if ( offer . getArbitratorSigner ( ) === arbitrator2ApiUrl && ! offer2 ) offer2 = offer ;
else await user1 . removeOffer ( offer . getId ( ) ) ;
if ( offer1 && offer2 ) break ;
2022-08-12 08:28:58 -04:00
}
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// complete a trade which uses arbitrator2 since it's least used
HavenoUtils . log ( 1 , "Completing trade using arbitrator2" ) ;
2023-10-28 10:20:56 -04:00
await executeTrade ( { maker : { havenod : user1 } , taker : { havenod : user2 } , arbitrator : { havenod : arbitrator2 } , offerId : offer1.getId ( ) , makerPaymentAccountId : offer1.getPaymentAccountId ( ) , testPayoutConfirmed : false } ) ;
2023-05-16 11:56:41 -04:00
let trade = await user1 . getTrade ( offer1 . getId ( ) ) ;
2022-08-12 08:28:58 -04:00
assert . equal ( trade . getArbitratorNodeAddress ( ) , arbitrator2ApiUrl ) ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// arbitrator2 goes offline without unregistering
HavenoUtils . log ( 1 , "Arbitrator2 going offline" ) ;
const arbitrator2AppName = arbitrator2 . getAppName ( )
await releaseHavenoProcess ( arbitrator2 ) ;
2022-12-13 08:44:20 +00:00
2023-05-16 11:56:41 -04:00
// post offer which uses main arbitrator since arbitrator2 is offline
HavenoUtils . log ( 1 , "Posting offer which uses main arbitrator since arbitrator2 is offline" ) ;
2023-10-28 10:20:56 -04:00
let offer = await makeOffer ( { maker : { havenod : user1 } } ) ;
2023-05-16 11:56:41 -04:00
assert . equal ( offer . getArbitratorSigner ( ) , arbitrator1ApiUrl ) ;
2023-02-24 11:58:33 -05:00
await user1 . removeOffer ( offer . getId ( ) ) ;
2022-12-13 08:44:20 +00:00
2023-05-16 11:56:41 -04:00
// complete a trade which uses main arbitrator since arbitrator2 is offline
HavenoUtils . log ( 1 , "Completing trade using main arbitrator since arbitrator2 is offline" ) ;
2023-10-28 10:20:56 -04:00
await executeTrade ( { maker : { havenod : user1 } , taker : { havenod : user2 } , offerId : offer2.getId ( ) , makerPaymentAccountId : offer2.getPaymentAccountId ( ) , testPayoutConfirmed : false } ) ;
2023-05-16 11:56:41 -04:00
trade = await user1 . getTrade ( offer2 . getId ( ) ) ;
assert . equal ( trade . getArbitratorNodeAddress ( ) , arbitrator1ApiUrl ) ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// start and unregister arbitrator2
HavenoUtils . log ( 1 , "Starting and unregistering arbitrator2" ) ;
arbitrator2 = await initHaveno ( { appName : arbitrator2AppName } ) ;
await arbitrator2 . unregisterDisputeAgent ( "arbitrator" ) ;
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// cannot take offers signed by unregistered arbitrator
HavenoUtils . log ( 1 , "Taking offer signed by unregistered arbitrator" ) ;
try {
2023-10-28 10:20:56 -04:00
await executeTrade ( { maker : { havenod : user1 } , taker : { havenod : user2 } , offerId : offer2.getId ( ) } ) ;
2022-08-12 08:28:58 -04:00
throw new Error ( "Should have failed taking offer signed by unregistered arbitrator" ) ;
2024-08-29 11:32:14 -04:00
} catch ( e2 : any ) {
2022-08-12 08:28:58 -04:00
assert ( e2 . message . indexOf ( "not found" ) > 0 ) ;
}
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// TODO: offer is removed and unreserved or re-signed, ideally keeping the same id
2022-12-13 08:44:20 +00:00
2023-05-16 11:56:41 -04:00
// post offer which uses main arbitrator since arbitrator2 is unregistered
2023-10-28 10:20:56 -04:00
offer = await makeOffer ( { maker : { havenod : user1 } } ) ;
2023-05-16 11:56:41 -04:00
assert . equal ( offer . getArbitratorSigner ( ) , arbitrator1ApiUrl ) ;
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs * 2 ) ;
2022-12-13 08:44:20 +00:00
2023-05-16 11:56:41 -04:00
// complete a trade which uses main arbitrator since arbitrator2 is unregistered
HavenoUtils . log ( 1 , "Completing trade with main arbitrator since arbitrator2 is unregistered" ) ;
2023-10-28 10:20:56 -04:00
await executeTrade ( { maker : { havenod : user1 } , taker : { havenod : user2 } , offerId : offer.getId ( ) , makerPaymentAccountId : offer.getPaymentAccountId ( ) , testPayoutConfirmed : false } ) ;
2023-05-16 11:56:41 -04:00
HavenoUtils . log ( 1 , "Done completing trade with main arbitrator since arbitrator2 is unregistered" ) ;
2022-08-12 08:28:58 -04:00
trade = await user2 . getTrade ( offer . getId ( ) ) ;
HavenoUtils . log ( 1 , "Done getting trade" ) ;
2023-05-16 11:56:41 -04:00
assert . equal ( trade . getArbitratorNodeAddress ( ) , arbitrator1ApiUrl ) ;
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// release arbitrator2
HavenoUtils . log ( 1 , "Done getting trade" ) ;
await releaseHavenoProcess ( arbitrator2 , true ) ;
} catch ( e ) {
err = e ;
}
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// cleanup if error
if ( err ) {
try { await arbitrator2 . unregisterDisputeAgent ( "arbitrator" ) ; }
catch ( err ) { /*ignore*/ }
await releaseHavenoProcess ( arbitrator2 , true ) ;
throw err ;
}
} ) ;
2024-07-16 16:10:13 -04:00
test ( "Can get trade statistics" , async ( ) = > {
const tradeStatisticsArbitrator = await arbitrator . getTradeStatistics ( ) ;
const tradeStatisticsUser1 = await user1 . getTradeStatistics ( ) ;
const tradeStatisticsUser2 = await user2 . getTradeStatistics ( ) ;
HavenoUtils . log ( 0 , "Trade statistics size (arb/u1/u2): " + tradeStatisticsArbitrator . length + "/" + tradeStatisticsUser1 . length + "/" + tradeStatisticsUser2 . length ) ;
assert ( tradeStatisticsArbitrator . length === tradeStatisticsUser1 . length && tradeStatisticsUser1 . length === tradeStatisticsUser2 . length ) ;
} ) ;
2022-11-04 15:57:42 -04:00
// ----------------------------- TEST HELPERS ---------------------------------
2021-09-17 09:33:58 -04:00
2022-11-04 15:57:42 -04:00
function getTradeContexts ( numConfigs : number ) : TradeContext [ ] {
const configs : TradeContext [ ] = [ ] ;
2023-10-28 10:20:56 -04:00
for ( let i = 0 ; i < numConfigs ; i ++ ) configs . push ( new TradeContext ( TestConfig . trade ) ) ;
2022-10-01 07:47:46 -04:00
return configs ;
2022-08-12 08:28:58 -04:00
}
2023-10-28 10:20:56 -04:00
async function executeTrades ( ctxs : Partial < TradeContext > [ ] , executionCtx? : Partial < TradeContext > ) : Promise < string [ ] > {
2022-11-28 13:15:04 +00:00
// assign default execution context
2022-12-20 23:28:58 +00:00
if ( ! executionCtx ) executionCtx = { } ;
2022-11-28 13:15:04 +00:00
if ( executionCtx . concurrentTrades === undefined ) executionCtx . concurrentTrades = ctxs . length > 1 ;
2023-10-28 10:20:56 -04:00
Object . assign ( executionCtx , new TradeContext ( TestConfig . trade ) , Object . assign ( { } , executionCtx ) ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// start mining if executing trades concurrently
2022-11-28 13:15:04 +00:00
let miningStarted = executionCtx . concurrentTrades && await startMining ( ) ;
2022-12-13 08:44:20 +00:00
2022-11-28 13:15:04 +00:00
// execute trades
2022-10-01 07:47:46 -04:00
let offerIds : string [ ] = [ ] ;
let err = undefined
try {
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// assign default configs
2023-10-28 10:20:56 -04:00
for ( let i = 0 ; i < ctxs . length ; i ++ ) Object . assign ( ctxs [ i ] , new TradeContext ( TestConfig . trade ) , Object . assign ( { index : i } , ctxs [ i ] ) ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// wait for traders to have unlocked balance for trades
2022-10-14 16:09:10 -04:00
let tradeAmount : bigint | undefined = undefined ;
2022-11-04 15:57:42 -04:00
const outputCounts = new Map < any , number > ( ) ;
for ( const ctx of ctxs ) {
2023-05-30 18:08:04 -04:00
if ( ! tradeAmount || tradeAmount < ctx . offerAmount ! ) tradeAmount = ctx . offerAmount ; // use max amount
2022-11-04 15:57:42 -04:00
if ( ctx . awaitFundsToMakeOffer && ctx . makeOffer && ! ctx . offerId ) {
2023-10-28 10:20:56 -04:00
const wallet = await getWallet ( ctx . maker ! . havenod ! ) ;
2022-11-04 15:57:42 -04:00
if ( outputCounts . has ( wallet ) ) outputCounts . set ( wallet , outputCounts . get ( wallet ) ! + 1 ) ;
else outputCounts . set ( wallet , 1 ) ;
}
if ( ctx . awaitFundsToTakeOffer && ctx . takeOffer && ! ctx . isOfferTaken ) {
2023-10-28 10:20:56 -04:00
const wallet = await getWallet ( ctx . taker ! . havenod ! ) ;
2022-11-04 15:57:42 -04:00
if ( outputCounts . has ( wallet ) ) outputCounts . set ( wallet , outputCounts . get ( wallet ) ! + 1 ) ;
else outputCounts . set ( wallet , 1 ) ;
}
}
const fundWalletPromises : Promise < void > [ ] = [ ] ;
for ( const wallet of outputCounts . keys ( ) ) {
if ( outputCounts . get ( wallet ) ! > 0 ) {
2023-12-27 16:58:30 -05:00
fundWalletPromises . push ( fundOutputs ( [ wallet ] , tradeAmount ! * 2 n , outputCounts . get ( wallet ) ) ) ;
2022-11-04 15:57:42 -04:00
}
}
await Promise . all ( fundWalletPromises ) ;
2023-02-15 10:59:57 -05:00
// execute trades in thread pool unless serial
2022-11-28 13:15:04 +00:00
if ( executionCtx . concurrentTrades ) {
2023-02-15 10:59:57 -05:00
const tradePromises : Promise < string > [ ] = [ ] ;
2023-10-02 08:16:54 -04:00
const pool = new moneroTs . ThreadPool ( executionCtx . maxConcurrency ! ) ;
2024-04-27 10:25:02 -04:00
for ( let i = 0 ; i < ctxs . length ; i ++ ) ctxs [ i ] = TradeContext . init ( Object . assign ( ctxs [ i ] , { concurrentTrades : executionCtx ! . concurrentTrades } ) ) ; // inititalize full trade contexts to avoid copy
for ( const ctx of ctxs ) tradePromises . push ( pool . submit ( ( ) = > executeTrade ( ctx ) ) ) ;
2023-02-15 10:59:57 -05:00
try {
offerIds = await Promise . all ( tradePromises ) ;
} catch ( e2 ) {
2023-12-23 09:03:02 -05:00
if ( executionCtx . stopOnFailure ) for ( const ctx of ctxs ) ctx . isStopped = true ; // stop trades on failure
try {
await Promise . allSettled ( tradePromises ) ; // wait for other trades to complete
2024-08-29 11:32:14 -04:00
} catch ( e3 : any ) {
2023-12-23 09:03:02 -05:00
HavenoUtils . log ( 0 , "Error awaiting other trades to stop after error: " + e3 . message ) ;
HavenoUtils . log ( 0 , e3 . stack ) ;
}
2023-02-15 10:59:57 -05:00
throw e2 ;
2022-10-01 07:47:46 -04:00
}
} else {
2022-11-28 13:15:04 +00:00
for ( const ctx of ctxs ) {
offerIds . push ( await executeTrade ( Object . assign ( ctx , { concurrentTrades : executionCtx.concurrentTrades } ) ) ) ;
2022-07-07 09:11:50 -04:00
}
}
2022-10-01 07:47:46 -04:00
} catch ( e ) {
err = e ;
2022-08-12 08:28:58 -04:00
}
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// stop mining if started, throw error or return offer ids
if ( miningStarted ) await stopMining ( ) ;
if ( err ) throw err ;
return offerIds ;
2022-07-07 09:11:50 -04:00
}
2022-10-01 07:47:46 -04:00
// TODO (woodser): test grpc notifications
2023-10-28 10:20:56 -04:00
async function executeTrade ( ctxP : Partial < TradeContext > ) : Promise < string > {
let ctx = TradeContext . init ( ctxP ) ;
2022-11-04 15:57:42 -04:00
try {
// fund maker and taker
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
const makingOffer = ctx . makeOffer && ! ctx . offerId ;
const clientsToFund : HavenoClient [ ] = [ ] ;
if ( ! ctx . concurrentTrades ) { // already funded
2023-10-28 10:20:56 -04:00
if ( ctx . awaitFundsToMakeOffer && makingOffer && ! ctx . offerId ) clientsToFund . push ( ctx . maker . havenod ! ) ;
if ( ctx . awaitFundsToTakeOffer && ctx . takeOffer && ! ctx . isOfferTaken ) clientsToFund . push ( ctx . taker . havenod ! ) ;
2023-12-27 16:58:30 -05:00
await waitForAvailableBalance ( ctx . offerAmount ! * 2 n , . . . clientsToFund ) ;
2022-10-01 07:47:46 -04:00
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// make offer if configured
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( makingOffer ) {
2023-01-23 16:17:00 -05:00
ctx . offer = await makeOffer ( ctx ) ;
2024-07-17 16:54:15 -04:00
expect ( ctx . offer . getState ( ) ) . toEqual ( ctx . reserveExactAmount ? "PENDING" : "AVAILABLE" ) ;
2023-01-23 16:17:00 -05:00
ctx . offerId = ctx . offer . getId ( ) ;
2023-10-28 10:20:56 -04:00
await wait ( ctx . maxTimePeerNoticeMs ) ;
2023-01-23 16:17:00 -05:00
} else {
2023-10-28 10:20:56 -04:00
ctx . offer = getOffer ( await ctx . maker . havenod ! . getMyOffers ( ctx . assetCode ! , ctx . direction ) , ctx . offerId ! ) ;
2023-01-23 16:17:00 -05:00
if ( ! ctx . offer ) {
2023-05-16 11:56:41 -04:00
try {
2023-10-28 10:20:56 -04:00
const trade = await ctx . maker . havenod ! . getTrade ( ctx . offerId ! ) ;
2023-05-16 11:56:41 -04:00
ctx . offer = trade . getOffer ( ) ;
} catch ( err ) { /* ignore */ }
2023-01-23 16:17:00 -05:00
}
2022-11-04 15:57:42 -04:00
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// TODO (woodser): test error message taking offer before posted
2022-12-13 08:44:20 +00:00
2023-06-10 10:02:15 -04:00
// wait for split output tx to unlock
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-07-23 12:23:10 -04:00
if ( ctx . reserveExactAmount ) {
2024-01-13 08:39:27 -05:00
const splitOutputTxId = ctx . offer ? . getSplitOutputTxHash ( ) ;
HavenoUtils . log ( 1 , "Waiting for split output tx " + splitOutputTxId + " to unlock" ) ;
if ( splitOutputTxId ) {
await mineToUnlock ( splitOutputTxId ) ;
await wait ( TestConfig . trade . walletSyncPeriodMs + TestConfig . trade . maxTimePeerNoticeMs ) ;
}
2023-06-10 10:02:15 -04:00
}
2022-11-04 15:57:42 -04:00
// take offer or get existing trade
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
let trade : TradeInfo | undefined = undefined ;
2023-10-28 10:20:56 -04:00
if ( ctx . isOfferTaken ) trade = await ctx . taker . havenod ! . getTrade ( ctx . offerId ! ) ;
2022-11-04 15:57:42 -04:00
else {
if ( ! ctx . takeOffer ) return ctx . offerId ! ;
trade = await takeOffer ( ctx ) ;
ctx . isOfferTaken = true ;
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// test trader chat
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-12-21 15:25:52 +00:00
if ( ctx . testTraderChat ) await testTradeChat ( ctx ) ;
2022-11-23 09:41:48 +00:00
// get expected payment account payloads
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
let expectedBuyerPaymentAccountPayload = ( await ctx . getBuyer ( ) . havenod ? . getPaymentAccount ( ctx . maker . havenod == ctx . getBuyer ( ) . havenod ? ctx . makerPaymentAccountId ! : ctx . takerPaymentAccountId ! ) ) ? . getPaymentAccountPayload ( ) ;
let expectedSellerPaymentAccountPayload = ( await ctx . getSeller ( ) . havenod ? . getPaymentAccount ( ctx . maker . havenod == ctx . getBuyer ( ) . havenod ? ctx . takerPaymentAccountId ! : ctx . makerPaymentAccountId ! ) ) ? . getPaymentAccountPayload ( ) ;
2022-11-23 09:41:48 +00:00
// seller does not have buyer's payment account payload until payment sent
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
let fetchedTrade = await ctx . getSeller ( ) . havenod ! . getTrade ( ctx . offerId ! ) ;
2022-11-23 09:41:48 +00:00
let contract = fetchedTrade . getContract ( ) ! ;
let buyerPaymentAccountPayload = contract . getIsBuyerMakerAndSellerTaker ( ) ? contract . getMakerPaymentAccountPayload ( ) : contract . getTakerPaymentAccountPayload ( ) ;
if ( ctx . isPaymentSent ) expect ( buyerPaymentAccountPayload ) . toEqual ( expectedBuyerPaymentAccountPayload ) ;
else expect ( buyerPaymentAccountPayload ) . toBeUndefined ( ) ;
2022-12-03 13:03:44 +00:00
2023-10-28 10:20:56 -04:00
// record context before payout
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
if ( ! ctx . isCompleted ) {
if ( ctx . maker . havenod ) ctx . maker . balancesBeforePayout = await ctx . maker . havenod ! . getBalances ( ) ;
if ( ctx . taker . havenod ) ctx . taker . balancesBeforePayout = await ctx . taker . havenod ! . getBalances ( ) ;
}
2022-11-04 15:57:42 -04:00
// shut down buyer and seller if configured
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
ctx . usedPorts = [ getPort ( ctx . getBuyer ( ) . havenod ! . getUrl ( ) ) , getPort ( ctx . getSeller ( ) . havenod ! . getUrl ( ) ) ] ;
2022-11-04 15:57:42 -04:00
const promises : Promise < void > [ ] = [ ] ;
2023-10-28 10:20:56 -04:00
ctx . buyerAppName = ctx . getBuyer ( ) . havenod ! . getAppName ( ) ;
2022-11-04 15:57:42 -04:00
if ( ctx . buyerOfflineAfterTake ) {
2024-04-24 21:19:25 -04:00
HavenoUtils . log ( 0 , "Buyer going offline" ) ;
2023-10-28 10:20:56 -04:00
promises . push ( releaseHavenoProcess ( ctx . getBuyer ( ) . havenod ! ) ) ;
if ( ctx . isBuyerMaker ( ) ) ctx . maker . havenod = undefined ;
else ctx . taker . havenod = undefined ;
2022-11-04 15:57:42 -04:00
}
2023-10-28 10:20:56 -04:00
ctx . sellerAppName = ctx . getSeller ( ) . havenod ! . getAppName ( ) ;
2022-11-04 15:57:42 -04:00
if ( ctx . sellerOfflineAfterTake ) {
2024-04-24 21:19:25 -04:00
HavenoUtils . log ( 0 , "Seller going offline" ) ;
2023-10-28 10:20:56 -04:00
promises . push ( releaseHavenoProcess ( ctx . getSeller ( ) . havenod ! ) ) ;
if ( ctx . isBuyerMaker ( ) ) ctx . taker . havenod = undefined ;
else ctx . maker . havenod = undefined ;
2022-11-04 15:57:42 -04:00
}
await Promise . all ( promises ) ;
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// wait for deposit txs to unlock
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
await waitForUnlockedTxs ( trade . getMakerDepositTxId ( ) , trade . getTakerDepositTxId ( ) ) ;
2022-12-13 08:44:20 +00:00
2023-01-23 16:17:00 -05:00
// buyer comes online if offline and used
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-01-23 16:17:00 -05:00
if ( ctx . buyerOfflineAfterTake && ( ( ctx . buyerSendsPayment && ! ctx . isPaymentSent && ctx . sellerDisputeContext !== DisputeContext . OPEN_AFTER_DEPOSITS_UNLOCK ) || ( ctx . buyerDisputeContext === DisputeContext . OPEN_AFTER_DEPOSITS_UNLOCK && ! ctx . buyerOpenedDispute ) ) ) {
2024-04-24 21:19:25 -04:00
HavenoUtils . log ( 0 , "Buyer coming online" ) ;
2023-10-28 10:20:56 -04:00
const buyer = await initHaveno ( { appName : ctx.buyerAppName , excludePorts : ctx.usedPorts } ) ; // change buyer's node address
if ( ctx . isBuyerMaker ( ) ) ctx . maker . havenod = buyer ;
else ctx . taker . havenod = buyer ;
2023-01-23 16:17:00 -05:00
ctx . usedPorts . push ( getPort ( buyer . getUrl ( ) ) ) ;
2022-11-04 15:57:42 -04:00
}
2022-12-13 08:44:20 +00:00
2023-01-23 16:17:00 -05:00
// wait for traders to observe
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
await wait ( TestConfig . maxWalletStartupMs + ctx . walletSyncPeriodMs * 2 ) ;
2023-01-23 16:17:00 -05:00
// test buyer trade state if online
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
const expectedState = ctx . isPaymentSent ? "PAYMENT_SENT" : "DEPOSITS_UNLOCKED" // TODO: test COMPLETED, PAYMENT_RECEIVED states?
2023-10-28 10:20:56 -04:00
if ( ctx . getBuyer ( ) . havenod ) {
expect ( ( await ctx . getBuyer ( ) . havenod ! . getTrade ( ctx . offer ! . getId ( ) ) ) . getPhase ( ) ) . toEqual ( expectedState ) ;
fetchedTrade = await ctx . getBuyer ( ) . havenod ! . getTrade ( ctx . offerId ! ) ;
2023-01-29 18:06:50 -05:00
expect ( fetchedTrade . getIsDepositsUnlocked ( ) ) . toBe ( true ) ;
2023-01-23 16:17:00 -05:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( expectedState ) ;
}
// test seller trade state if online
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
if ( ctx . getSeller ( ) . havenod ) {
fetchedTrade = await ctx . getSeller ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) ;
2023-01-29 18:06:50 -05:00
expect ( fetchedTrade . getIsDepositsUnlocked ( ) ) . toBe ( true ) ;
2022-11-04 15:57:42 -04:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( expectedState ) ;
2022-10-01 07:47:46 -04:00
}
2022-11-23 09:41:48 +00:00
// buyer has seller's payment account payload after first confirmation
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2024-08-29 11:32:14 -04:00
let sellerPaymentAccountPayload : PaymentAccountPayload | undefined ;
2023-01-23 16:17:00 -05:00
let form ;
let expectedForm ;
2023-10-28 10:20:56 -04:00
if ( ctx . getBuyer ( ) . havenod ) {
fetchedTrade = await ctx . getBuyer ( ) . havenod ! . getTrade ( ctx . offerId ! ) ;
2023-01-23 16:17:00 -05:00
contract = fetchedTrade . getContract ( ) ! ;
sellerPaymentAccountPayload = contract . getIsBuyerMakerAndSellerTaker ( ) ? contract . getTakerPaymentAccountPayload ( ) : contract . getMakerPaymentAccountPayload ( ) ;
expect ( sellerPaymentAccountPayload ) . toEqual ( expectedSellerPaymentAccountPayload ) ;
2023-10-28 10:20:56 -04:00
form = await ctx . getBuyer ( ) . havenod ! . getPaymentAccountPayloadForm ( sellerPaymentAccountPayload ! ) ;
expectedForm = await ctx . getBuyer ( ) . havenod ! . getPaymentAccountPayloadForm ( expectedSellerPaymentAccountPayload ! ) ;
2023-01-23 16:17:00 -05:00
expect ( HavenoUtils . formToString ( form ) ) . toEqual ( HavenoUtils . formToString ( expectedForm ) ) ;
}
2022-11-23 09:41:48 +00:00
2022-11-04 15:57:42 -04:00
// buyer notified to send payment TODO
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// open dispute(s) if configured
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . buyerDisputeContext === DisputeContext . OPEN_AFTER_DEPOSITS_UNLOCK && ! ctx . buyerOpenedDispute ) {
2023-10-28 10:20:56 -04:00
await ctx . getBuyer ( ) . havenod ! . openDispute ( ctx . offerId ! ) ;
2022-11-04 15:57:42 -04:00
ctx . buyerOpenedDispute = true ;
2023-01-23 16:17:00 -05:00
ctx . disputeOpener = SaleRole . BUYER ;
2022-10-01 07:47:46 -04:00
}
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . sellerDisputeContext === DisputeContext . OPEN_AFTER_DEPOSITS_UNLOCK && ! ctx . sellerOpenedDispute ) {
2023-10-28 10:20:56 -04:00
await ctx . getSeller ( ) . havenod ! . openDispute ( ctx . offerId ! ) ;
2022-11-04 15:57:42 -04:00
ctx . sellerOpenedDispute = true ;
2023-01-23 16:17:00 -05:00
if ( ! ctx . disputeOpener ) ctx . disputeOpener = SaleRole . SELLER ;
2022-11-04 15:57:42 -04:00
}
2023-01-23 16:17:00 -05:00
// handle opened dispute
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . disputeOpener ) {
2023-01-23 16:17:00 -05:00
// test open dispute
2022-11-04 15:57:42 -04:00
await testOpenDispute ( ctx ) ;
2022-12-13 08:44:20 +00:00
2023-01-23 16:17:00 -05:00
// resolve dispute if configured
2022-11-04 15:57:42 -04:00
if ( ctx . resolveDispute ) await resolveDispute ( ctx ) ;
2023-01-23 16:17:00 -05:00
// return offer id
2022-11-04 15:57:42 -04:00
return ctx . offerId ! ;
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// buyer confirms payment is sent
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-01-23 16:17:00 -05:00
if ( ! ctx . buyerSendsPayment ) return ctx . offer ! . getId ( ) ;
2022-11-04 15:57:42 -04:00
else if ( ! ctx . isPaymentSent ) {
HavenoUtils . log ( 1 , "Buyer confirming payment sent" ) ;
2023-10-28 10:20:56 -04:00
await ctx . getBuyer ( ) . havenod ! . confirmPaymentSent ( trade . getTradeId ( ) ) ;
2022-11-04 15:57:42 -04:00
ctx . isPaymentSent = true ;
2023-10-28 10:20:56 -04:00
fetchedTrade = await ctx . getBuyer ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) ;
2022-11-04 15:57:42 -04:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "PAYMENT_SENT" ) ;
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// buyer goes offline if configured
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . buyerOfflineAfterPaymentSent ) {
2024-04-24 21:19:25 -04:00
HavenoUtils . log ( 0 , "Buyer going offline" ) ;
2023-10-28 10:20:56 -04:00
await releaseHavenoProcess ( ctx . getBuyer ( ) . havenod ! ) ;
if ( ctx . isBuyerMaker ( ) ) ctx . maker . havenod = undefined ;
else ctx . taker . havenod = undefined ;
2022-11-04 15:57:42 -04:00
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// seller comes online if offline
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
if ( ! ctx . getSeller ( ) . havenod ) {
2024-04-24 21:19:25 -04:00
HavenoUtils . log ( 0 , "Seller coming online" ) ;
2023-01-23 16:17:00 -05:00
const seller = await initHaveno ( { appName : ctx.sellerAppName , excludePorts : ctx.usedPorts } ) ;
2023-10-28 10:20:56 -04:00
if ( ctx . isBuyerMaker ( ) ) ctx . taker . havenod = seller ;
else ctx . maker . havenod = seller ;
ctx . usedPorts . push ( getPort ( ctx . getSeller ( ) . havenod ! . getUrl ( ) ) )
2022-11-04 15:57:42 -04:00
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// seller notified payment is sent
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
await wait ( ctx . maxTimePeerNoticeMs + TestConfig . maxWalletStartupMs ) ; // TODO: test notification
if ( ctx . sellerOfflineAfterTake ) await wait ( ctx . walletSyncPeriodMs ) ; // wait to process mailbox messages
fetchedTrade = await ctx . getSeller ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) ;
2022-11-04 15:57:42 -04:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "PAYMENT_SENT" ) ;
expect ( fetchedTrade . getPayoutState ( ) ) . toEqual ( "PAYOUT_UNPUBLISHED" ) ;
2022-11-23 09:41:48 +00:00
// seller has buyer's payment account payload after payment sent
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
fetchedTrade = await ctx . getSeller ( ) . havenod ! . getTrade ( ctx . offerId ! ) ;
2022-11-23 09:41:48 +00:00
contract = fetchedTrade . getContract ( ) ! ;
buyerPaymentAccountPayload = contract . getIsBuyerMakerAndSellerTaker ( ) ? contract . getMakerPaymentAccountPayload ( ) : contract . getTakerPaymentAccountPayload ( ) ;
expect ( buyerPaymentAccountPayload ) . toEqual ( expectedBuyerPaymentAccountPayload ) ;
2023-10-28 10:20:56 -04:00
form = await ctx . getSeller ( ) . havenod ! . getPaymentAccountPayloadForm ( sellerPaymentAccountPayload ! ) ;
expectedForm = await ctx . getSeller ( ) . havenod ! . getPaymentAccountPayloadForm ( expectedSellerPaymentAccountPayload ! ) ;
2022-11-23 09:41:48 +00:00
expect ( HavenoUtils . formToString ( form ) ) . toEqual ( HavenoUtils . formToString ( expectedForm ) ) ;
2022-11-04 15:57:42 -04:00
// open dispute(s) if configured
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . buyerDisputeContext === DisputeContext . OPEN_AFTER_PAYMENT_SENT && ! ctx . buyerOpenedDispute ) {
2023-10-28 10:20:56 -04:00
await ctx . getBuyer ( ) . havenod ! . openDispute ( ctx . offerId ! ) ;
2022-11-04 15:57:42 -04:00
ctx . buyerOpenedDispute = true ;
2023-01-23 16:17:00 -05:00
if ( ! ctx . disputeOpener ) ctx . disputeOpener = SaleRole . BUYER ;
2022-11-04 15:57:42 -04:00
}
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . sellerDisputeContext === DisputeContext . OPEN_AFTER_PAYMENT_SENT && ! ctx . sellerOpenedDispute ) {
2023-10-28 10:20:56 -04:00
await ctx . getSeller ( ) . havenod ! . openDispute ( ctx . offerId ! ) ;
2022-11-04 15:57:42 -04:00
ctx . sellerOpenedDispute = true ;
2023-01-23 16:17:00 -05:00
if ( ! ctx . disputeOpener ) ctx . disputeOpener = SaleRole . SELLER ;
2022-11-04 15:57:42 -04:00
}
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
if ( ctx . disputeOpener ) await testOpenDispute ( ctx ) ;
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// if dispute opened, resolve dispute if configured and return
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . disputeOpener ) {
if ( ctx . resolveDispute ) await resolveDispute ( ctx ) ;
return ctx . offerId ! ;
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// seller confirms payment is received
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-01-23 16:17:00 -05:00
if ( ! ctx . sellerReceivesPayment ) return ctx . offer ! . getId ( ) ;
2022-11-04 15:57:42 -04:00
else if ( ! ctx . isPaymentReceived ) {
HavenoUtils . log ( 1 , "Seller confirming payment received" ) ;
2023-10-28 10:20:56 -04:00
await ctx . getSeller ( ) . havenod ! . confirmPaymentReceived ( trade . getTradeId ( ) ) ;
2022-11-04 15:57:42 -04:00
ctx . isPaymentReceived = true ;
2023-10-28 10:20:56 -04:00
fetchedTrade = await ctx . getSeller ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) ;
2022-11-04 15:57:42 -04:00
expect ( fetchedTrade . getPhase ( ) ) . toEqual ( "PAYMENT_RECEIVED" ) ;
2023-10-28 10:20:56 -04:00
await wait ( ctx . walletSyncPeriodMs * 2 ) ; // buyer or arbitrator will sign and publish payout tx
await testTradeState ( await ctx . getSeller ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) , { phase : "PAYMENT_RECEIVED" , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , isCompleted : false , isPayoutPublished : true } ) ;
2022-11-04 15:57:42 -04:00
}
// payout tx is published by buyer (priority) or arbitrator
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
await wait ( ctx . walletSyncPeriodMs ) ;
await testTradeState ( await ctx . getSeller ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) , { phase : "PAYMENT_RECEIVED" , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , isCompleted : false , isPayoutPublished : true } ) ;
2023-12-15 09:24:23 -05:00
await testTradeState ( await ctx . arbitrator . havenod ! . getTrade ( trade . getTradeId ( ) ) , { phase : "PAYMENT_RECEIVED" , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , isCompleted : true , isPayoutPublished : true } ) ; // arbitrator trade auto completes
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// buyer comes online if offline
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
if ( ctx . buyerOfflineAfterPaymentSent ) {
2024-04-26 13:25:29 -04:00
HavenoUtils . log ( 0 , "Buyer coming online" ) ;
2023-01-23 16:17:00 -05:00
const buyer = await initHaveno ( { appName : ctx.buyerAppName , excludePorts : ctx.usedPorts } ) ;
2023-10-28 10:20:56 -04:00
if ( ctx . isBuyerMaker ( ) ) ctx . maker . havenod = buyer ;
else ctx . taker . havenod = buyer ;
2023-01-23 16:17:00 -05:00
ctx . usedPorts . push ( getPort ( buyer . getUrl ( ) ) ) ;
2022-11-04 15:57:42 -04:00
HavenoUtils . log ( 1 , "Done starting buyer" ) ;
2023-10-28 10:20:56 -04:00
await wait ( TestConfig . maxWalletStartupMs + ctx . walletSyncPeriodMs ) ;
2022-11-04 15:57:42 -04:00
}
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
await testTradeState ( await ctx . getBuyer ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) , { phase : "PAYMENT_RECEIVED" , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , isCompleted : false , isPayoutPublished : true } ) ;
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// test trade completion
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
await ctx . getBuyer ( ) . havenod ! . completeTrade ( trade . getTradeId ( ) ) ;
2023-12-15 09:24:23 -05:00
await testTradeState ( await ctx . getBuyer ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) , { phase : "PAYMENT_RECEIVED" , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , isCompleted : true , isPayoutPublished : true } ) ;
2023-10-28 10:20:56 -04:00
await ctx . getSeller ( ) . havenod ! . completeTrade ( trade . getTradeId ( ) ) ;
2023-12-15 09:24:23 -05:00
await testTradeState ( await ctx . getSeller ( ) . havenod ! . getTrade ( trade . getTradeId ( ) ) , { phase : "PAYMENT_RECEIVED" , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , isCompleted : true , isPayoutPublished : true } ) ;
2023-10-28 10:20:56 -04:00
// record balances on completion
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-10-28 10:20:56 -04:00
if ( ! ctx . maker . balancesAfterPayout ) {
ctx . maker . balancesAfterPayout = await ctx . maker . havenod ? . getBalances ( ) ;
ctx . taker . balancesAfterPayout = await ctx . taker . havenod ? . getBalances ( ) ;
2024-09-25 09:40:31 -04:00
// record payout tx id
ctx . payoutTxId = ( await ctx . getSeller ( ) . havenod ! . getTrade ( ctx . offerId ! ) ) . getPayoutTxId ( ) ;
if ( ! ctx . payoutTxId ) ctx . payoutTxId = ( await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) ) . getPayoutTxId ( ) ; // TODO: arbitrator will sign and publish payout tx id if buyer is offline; detect payout tx id on 0 conf
if ( ! ctx . payoutTxId ) ctx . payoutTxId = ( await ctx . getBuyer ( ) . havenod ! . getTrade ( ctx . offerId ! ) ) . getPayoutTxId ( ) ; // TODO: arbitrator does not have payout tx id until first confirmation because they defer publishing
2023-10-28 10:20:56 -04:00
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// test balances after payout tx unless other trades can interfere
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2023-12-07 18:05:15 -05:00
if ( ! ctx . concurrentTrades ) await testAmountsAfterComplete ( ctx ) ;
2022-11-04 15:57:42 -04:00
// test payout unlock
2023-12-23 09:03:02 -05:00
if ( ctx . isStopped ) return ctx . offerId ! ;
2022-11-04 15:57:42 -04:00
await testTradePayoutUnlock ( ctx ) ;
2023-01-23 16:17:00 -05:00
if ( ctx . offer ! . getId ( ) !== ctx . offerId ) throw new Error ( "Expected offer ids to match" ) ;
return ctx . offer ! . getId ( ) ;
2024-08-29 11:32:14 -04:00
} catch ( err : any ) {
2022-11-04 15:57:42 -04:00
HavenoUtils . log ( 0 , "Error executing trade " + ctx ! . offerId + ( ctx ! . index === undefined ? "" : " at index " + ctx ! . index ) + ": " + err . message ) ;
2023-10-28 10:20:56 -04:00
HavenoUtils . log ( 0 , await ctx . toSummary ( ) ) ;
2022-11-04 15:57:42 -04:00
throw err ;
2022-10-01 07:47:46 -04:00
}
2022-11-04 15:57:42 -04:00
}
2023-10-28 10:20:56 -04:00
async function testTradePayoutUnlock ( ctxP : Partial < TradeContext > ) {
let ctx = TradeContext . init ( ctxP ) ;
2022-11-23 14:24:52 +00:00
// test after payout confirmed
2023-05-11 08:56:59 -04:00
if ( ! ctx . testPayoutConfirmed ) return ;
const height = await monerod . getHeight ( ) ;
2023-10-28 10:20:56 -04:00
const payoutTxId = ( await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) ) . getPayoutTxId ( ) ;
let trade = await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) ;
2022-11-27 18:31:33 +00:00
if ( trade . getPayoutState ( ) !== "PAYOUT_CONFIRMED" ) await mineToHeight ( height + 1 ) ;
2023-10-28 10:20:56 -04:00
await wait ( TestConfig . maxWalletStartupMs + ctx . walletSyncPeriodMs * 2 ) ;
2023-12-15 09:24:23 -05:00
const disputeState = ctx . isPaymentReceived ? "NO_DISPUTE" : "DISPUTE_CLOSED" ;
if ( ctx . getBuyer ( ) . havenod ) await testTradeState ( await ctx . getBuyer ( ) . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , disputeState : disputeState , payoutState : [ "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] } ) ;
if ( ctx . getSeller ( ) . havenod ) await testTradeState ( await ctx . getSeller ( ) . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , disputeState : disputeState , payoutState : [ "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] } ) ;
await testTradeState ( await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , payoutState : [ "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] } ) ;
2023-10-28 10:20:56 -04:00
let payoutTx = ctx . getBuyer ( ) . havenod ? await ctx . getBuyer ( ) . havenod ? . getXmrTx ( payoutTxId ) : await ctx . getSeller ( ) . havenod ? . getXmrTx ( payoutTxId ) ;
2022-11-27 18:31:33 +00:00
expect ( payoutTx ? . getIsConfirmed ( ) ) ;
2022-12-13 08:44:20 +00:00
2022-11-23 14:24:52 +00:00
// test after payout unlocked
2023-05-11 08:56:59 -04:00
if ( ! ctx . testPayoutUnlocked ) return ;
2023-10-28 10:20:56 -04:00
trade = await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) ;
2022-11-27 18:31:33 +00:00
if ( trade . getPayoutState ( ) !== "PAYOUT_UNLOCKED" ) await mineToHeight ( height + 10 ) ;
2023-10-28 10:20:56 -04:00
await wait ( TestConfig . maxWalletStartupMs + ctx . walletSyncPeriodMs * 2 ) ;
2023-12-15 09:24:23 -05:00
if ( await ctx . getBuyer ( ) . havenod ) await testTradeState ( await ctx . getBuyer ( ) . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , disputeState : disputeState , payoutState : [ "PAYOUT_UNLOCKED" ] } ) ;
if ( await ctx . getSeller ( ) . havenod ) await testTradeState ( await ctx . getSeller ( ) . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , disputeState : disputeState , payoutState : [ "PAYOUT_UNLOCKED" ] } ) ;
await testTradeState ( await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , disputeState : disputeState , payoutState : [ "PAYOUT_UNLOCKED" ] } ) ;
2023-10-28 10:20:56 -04:00
payoutTx = ctx . getBuyer ( ) . havenod ? await ctx . getBuyer ( ) . havenod ? . getXmrTx ( payoutTxId ) : await ctx . getSeller ( ) . havenod ? . getXmrTx ( payoutTxId ) ;
2022-11-27 18:31:33 +00:00
expect ( ! payoutTx ? . getIsLocked ( ) ) ;
2022-10-26 01:05:49 -04:00
}
2023-10-28 10:20:56 -04:00
async function testTradeState ( trade : TradeInfo , ctx : Partial < TradeContext > ) {
2022-11-04 15:57:42 -04:00
assert . equal ( trade . getPhase ( ) , ctx . phase , "expected trade phase to be " + ctx . phase + " but was " + trade . getPhase ( ) + " for trade " + trade . getTradeId ( ) ) ;
2023-10-02 08:16:54 -04:00
assert ( moneroTs . GenUtils . arrayContains ( ctx . payoutState , trade . getPayoutState ( ) ) , "expected one of payout state " + ctx . payoutState + " but was " + trade . getPayoutState ( ) + " for trade " + trade . getTradeId ( ) ) ;
2022-11-04 15:57:42 -04:00
if ( ctx . disputeState ) expect ( trade . getDisputeState ( ) ) . toEqual ( ctx . disputeState ) ;
if ( ctx . isCompleted !== undefined ) expect ( trade . getIsCompleted ( ) ) . toEqual ( ctx . isCompleted ) ;
if ( ctx . isPayoutPublished !== undefined ) expect ( trade . getIsPayoutPublished ( ) ) . toEqual ( ctx . isPayoutPublished ) ;
if ( ctx . isPayoutConfirmed !== undefined ) expect ( trade . getIsPayoutConfirmed ( ) ) . toEqual ( ctx . isPayoutConfirmed ) ;
2023-12-15 09:24:23 -05:00
if ( ctx . isPayoutConfirmed ) expect ( trade . getIsPayoutPublished ( ) ) . toEqual ( true ) ;
2022-11-04 15:57:42 -04:00
if ( ctx . isPayoutUnlocked !== undefined ) expect ( trade . getIsPayoutUnlocked ( ) ) . toEqual ( ctx . isPayoutUnlocked ) ;
2023-12-15 09:24:23 -05:00
if ( ctx . isPayoutUnlocked ) {
expect ( trade . getIsPayoutConfirmed ( ) ) . toEqual ( true ) ;
expect ( trade . getIsPayoutPublished ( ) ) . toEqual ( true ) ;
}
2022-10-26 01:05:49 -04:00
}
2023-10-28 10:20:56 -04:00
async function makeOffer ( ctxP? : Partial < TradeContext > ) : Promise < OfferInfo > {
let ctx = TradeContext . init ( ctxP ) ;
2022-10-01 07:47:46 -04:00
// wait for unlocked balance
2023-12-27 16:58:30 -05:00
if ( ! ctx . concurrentTrades && ctx . awaitFundsToMakeOffer ) await waitForAvailableBalance ( ctx . offerAmount ! * 2 n , ctx . maker . havenod ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// create payment account if not given // TODO: re-use existing payment account
2023-10-28 10:20:56 -04:00
if ( ! ctx . makerPaymentAccountId ) ctx . makerPaymentAccountId = ( await createPaymentAccount ( ctx . maker . havenod ! , ctx . assetCode ! ) ) . getId ( ) ;
2022-11-23 09:41:48 +00:00
2022-10-01 07:47:46 -04:00
// get unlocked balance before reserving offer
2023-10-28 10:20:56 -04:00
let unlockedBalanceBefore = BigInt ( ( await ctx . maker . havenod ! . getBalances ( ) ) . getAvailableBalance ( ) ) ;
2023-12-27 16:58:30 -05:00
if ( ctx . awaitFundsToMakeOffer && unlockedBalanceBefore === 0 n ) {
2022-11-23 09:41:48 +00:00
HavenoUtils . log ( 0 , "WARNING: unlocked balance before posting offer is 0, waiting..." ) ;
await wait ( 5000 ) ;
2023-10-28 10:20:56 -04:00
unlockedBalanceBefore = BigInt ( ( await ctx . maker . havenod ! . getBalances ( ) ) . getAvailableBalance ( ) ) ;
2023-12-27 16:58:30 -05:00
if ( unlockedBalanceBefore === 0 n ) throw new Error ( "Unlocked balance before posting offer was 0, even after waiting" ) ;
2022-11-23 09:41:48 +00:00
}
2022-12-13 08:44:20 +00:00
2023-10-28 10:20:56 -04:00
// initialize balances before offer, once
if ( ! ctx . maker . balancesBeforeOffer ) {
ctx . maker . balancesBeforeOffer = await ctx . maker . havenod ? . getBalances ( ) ;
ctx . taker . balancesBeforeOffer = await ctx . taker . havenod ? . getBalances ( ) ;
}
2022-10-01 07:47:46 -04:00
// post offer
2023-10-28 10:20:56 -04:00
const offer : OfferInfo = await ctx . maker . havenod ! . postOffer (
2022-11-04 15:57:42 -04:00
ctx . direction ! ,
2023-05-30 18:08:04 -04:00
ctx . offerAmount ! ,
2022-11-04 15:57:42 -04:00
ctx . assetCode ! ,
ctx . makerPaymentAccountId ! ,
2023-10-30 17:11:33 -04:00
ctx . securityDepositPct ! ,
2022-11-04 15:57:42 -04:00
ctx . price ,
ctx . priceMargin ,
ctx . triggerPrice ,
2023-07-26 18:58:48 -04:00
ctx . offerMinAmount ,
ctx . reserveExactAmount ) ;
2022-11-04 15:57:42 -04:00
testOffer ( offer , ctx ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// offer is included in my offers only
2023-10-28 10:20:56 -04:00
if ( ! getOffer ( await ctx . maker . havenod ! . getMyOffers ( ctx . assetCode ! , ctx . direction ) , offer . getId ( ) ) ) {
2022-10-01 07:47:46 -04:00
console . warn ( "Offer is not included in my offers after posting, waiting up to 10 seconds" ) ;
await wait ( 10000 ) ; // TODO: remove this wait time
2023-10-28 10:20:56 -04:00
if ( ! getOffer ( await ctx . maker . havenod ! . getMyOffers ( ctx . assetCode ! , ctx . direction ) , offer . getId ( ) ) ) throw new Error ( "Offer " + offer . getId ( ) + " was not found in my offers" ) ;
2022-10-01 07:47:46 -04:00
}
2023-10-28 10:20:56 -04:00
if ( getOffer ( await ctx . maker . havenod ! . getOffers ( ctx . assetCode ! , ctx . direction ) , offer . getId ( ) ) ) throw new Error ( "My offer " + offer . getId ( ) + " should not appear in available offers" ) ;
2022-12-13 08:44:20 +00:00
2023-10-28 10:20:56 -04:00
// collect context
ctx . maker . splitOutputTxFee = BigInt ( offer . getSplitOutputTxFee ( ) ) ;
ctx . taker . splitOutputTxFee = 0 n ;
// market-priced offer amounts are unadjusted, fixed-priced offer amounts are adjusted (e.g. cash at atm is $10 increments)
// TODO: adjustments should be based on currency and payment method, not fixed-price
2023-09-09 10:25:52 -04:00
if ( ! ctx . offerMinAmount ) ctx . offerMinAmount = ctx . offerAmount ;
if ( offer . getUseMarketBasedPrice ( ) ) {
2023-10-28 10:20:56 -04:00
expect ( BigInt ( offer . getAmount ( ) ) ) . toEqual ( ctx . offerAmount ! ) ;
expect ( BigInt ( offer . getMinAmount ( ) ) ) . toEqual ( ctx . offerMinAmount ! ) ;
2023-09-09 10:25:52 -04:00
} else {
expect ( Math . abs ( HavenoUtils . percentageDiff ( ctx . offerAmount ! , BigInt ( offer . getAmount ( ) ) ) ) ) . toBeLessThan ( TestConfig . maxAdjustmentPct ) ;
expect ( Math . abs ( HavenoUtils . percentageDiff ( ctx . offerMinAmount ! , BigInt ( offer . getMinAmount ( ) ) ) ) ) . toBeLessThan ( TestConfig . maxAdjustmentPct ) ;
}
2022-10-01 07:47:46 -04:00
// unlocked balance has decreased
2023-10-28 10:20:56 -04:00
let unlockedBalanceAfter = BigInt ( ( await ctx . maker . havenod ! . getBalances ( ) ) . getAvailableBalance ( ) ) ;
2024-07-17 16:54:15 -04:00
if ( offer . getState ( ) === "PENDING" ) {
2023-07-23 12:23:10 -04:00
if ( ! ctx . reserveExactAmount && unlockedBalanceAfter !== unlockedBalanceBefore ) throw new Error ( "Unlocked balance should not change for scheduled offer " + offer . getId ( ) ) ;
2022-10-01 07:47:46 -04:00
} else if ( offer . getState ( ) === "AVAILABLE" ) {
2023-02-10 11:21:46 -05:00
if ( unlockedBalanceAfter === unlockedBalanceBefore ) {
console . warn ( "Unlocked balance did not change after posting offer, waiting a sync period" ) ;
2023-10-28 10:20:56 -04:00
await wait ( ctx . walletSyncPeriodMs ) ;
unlockedBalanceAfter = BigInt ( ( await ctx . maker . havenod ! . getBalances ( ) ) . getAvailableBalance ( ) ) ;
2023-02-10 11:21:46 -05:00
if ( unlockedBalanceAfter === unlockedBalanceBefore ) throw new Error ( "Unlocked balance did not change after posting offer " + offer . getId ( ) + ", before=" + unlockedBalanceBefore + ", after=" + unlockedBalanceAfter ) ;
}
2022-10-01 07:47:46 -04:00
} else {
throw new Error ( "Unexpected offer state after posting: " + offer . getState ( ) ) ;
}
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
return offer ;
}
2023-10-28 10:20:56 -04:00
async function takeOffer ( ctxP : Partial < TradeContext > ) : Promise < TradeInfo > {
let ctx = TradeContext . init ( ctxP ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// assign default config
2023-10-28 10:20:56 -04:00
Object . assign ( ctx , new TradeContext ( TestConfig . trade ) , Object . assign ( { } , ctx ) ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// taker sees offer
2022-11-04 15:57:42 -04:00
if ( ! ctx . offerId ) throw new Error ( "Must provide offer id" ) ;
2023-10-28 10:20:56 -04:00
const takerOffer = getOffer ( await ctx . taker . havenod ! . getOffers ( ctx . assetCode ! , ctx . direction ) , ctx . offerId ) ;
2022-11-23 09:41:48 +00:00
if ( ! takerOffer ) throw new Error ( "Offer " + ctx . offerId + " was not found in taker's offers" ) ;
2022-10-01 07:47:46 -04:00
expect ( takerOffer . getState ( ) ) . toEqual ( "UNKNOWN" ) ; // TODO: offer state should be known
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// wait for unlocked balance
2023-12-27 16:58:30 -05:00
if ( ctx . awaitFundsToTakeOffer ) await waitForAvailableBalance ( ctx . offerAmount ! * 2 n , ctx . taker . havenod ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// create payment account if not given // TODO: re-use existing payment account
2023-10-28 10:20:56 -04:00
if ( ! ctx . takerPaymentAccountId ) ctx . takerPaymentAccountId = ( await createPaymentAccount ( ctx . taker . havenod ! , ctx . assetCode ! ) ) . getId ( ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// register to receive notifications
const makerNotifications : NotificationMessage [ ] = [ ] ;
const takerNotifications : NotificationMessage [ ] = [ ] ;
2023-10-28 10:20:56 -04:00
await ctx . maker . havenod ! . addNotificationListener ( notification = > { makerNotifications . push ( notification ) ; } ) ;
await ctx . taker . havenod ! . addNotificationListener ( notification = > { takerNotifications . push ( notification ) ; } ) ;
// record balances before offer taken, once
2023-12-28 07:59:55 -05:00
if ( ctx . taker . balancesBeforeTake === undefined ) {
2023-10-28 10:20:56 -04:00
ctx . maker . balancesBeforeTake = await ctx . maker . havenod ? . getBalances ( ) ;
ctx . taker . balancesBeforeTake = await ctx . taker . havenod ? . getBalances ( ) ;
}
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// take offer
2023-10-28 10:20:56 -04:00
const takerBalancesBefore : XmrBalanceInfo = await ctx . taker . havenod ! . getBalances ( ) ;
2022-10-01 07:47:46 -04:00
const startTime = Date . now ( ) ;
2022-11-04 15:57:42 -04:00
HavenoUtils . log ( 1 , "Taking offer " + ctx . offerId ) ;
2023-10-28 10:20:56 -04:00
const trade = await ctx . taker . havenod ! . takeOffer ( ctx . offerId , ctx . takerPaymentAccountId ! , ctx . tradeAmount ) ;
2022-11-05 19:32:28 -04:00
HavenoUtils . log ( 1 , "Done taking offer " + ctx . offerId + " in " + ( Date . now ( ) - startTime ) + " ms" ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// maker is notified that offer is taken
2023-10-28 10:20:56 -04:00
await wait ( ctx . maxTimePeerNoticeMs ) ;
2022-10-01 07:47:46 -04:00
const tradeNotifications = getNotifications ( makerNotifications , NotificationMessage . NotificationType . TRADE_UPDATE , trade . getTradeId ( ) ) ;
expect ( tradeNotifications . length ) . toBe ( 1 ) ;
2023-10-02 08:16:54 -04:00
assert ( moneroTs . GenUtils . arrayContains ( [ "DEPOSITS_PUBLISHED" , "DEPOSITS_CONFIRMED" , "DEPOSITS_UNLOCKED" ] , tradeNotifications [ 0 ] . getTrade ( ) ! . getPhase ( ) ) , "Unexpected trade phase: " + tradeNotifications [ 0 ] . getTrade ( ) ! . getPhase ( ) ) ;
2022-10-01 07:47:46 -04:00
expect ( tradeNotifications [ 0 ] . getTitle ( ) ) . toEqual ( "Offer Taken" ) ;
2022-11-04 15:57:42 -04:00
expect ( tradeNotifications [ 0 ] . getMessage ( ) ) . toEqual ( "Your offer " + ctx . offerId + " has been accepted" ) ;
2022-12-13 08:44:20 +00:00
2024-07-17 17:41:47 -04:00
// set context after offer taken, once
2023-12-28 07:59:55 -05:00
if ( ctx . getBuyer ( ) . balancesAfterTake === undefined ) {
2024-01-07 16:15:48 -05:00
// wait to observe deposit txs
2023-10-28 10:20:56 -04:00
ctx . arbitrator . trade = await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) ;
2023-12-23 09:03:02 -05:00
ctx . maker . depositTx = await monerod . getTx ( ctx . arbitrator . trade ! . getMakerDepositTxId ( ) ) ;
ctx . taker . depositTx = await monerod . getTx ( ctx . arbitrator . trade ! . getTakerDepositTxId ( ) ) ;
if ( ! ctx . maker . depositTx || ! ctx . taker . depositTx ) {
if ( ! ctx . maker . depositTx ) HavenoUtils . log ( 0 , "Maker deposit tx not found with id " + ctx . arbitrator . trade ! . getMakerDepositTxId ( ) + ", waiting..." ) ;
if ( ! ctx . taker . depositTx ) HavenoUtils . log ( 0 , "Taker deposit tx not found with id " + ctx . arbitrator . trade ! . getTakerDepositTxId ( ) + ", waiting..." ) ;
await wait ( ctx . walletSyncPeriodMs ) ;
ctx . maker . depositTx = await monerod . getTx ( ctx . arbitrator . trade ! . getMakerDepositTxId ( ) ) ;
ctx . taker . depositTx = await monerod . getTx ( ctx . arbitrator . trade ! . getTakerDepositTxId ( ) ) ;
if ( ! ctx . maker . depositTx ) throw new Error ( "Maker deposit tx not found with id " + ctx . arbitrator . trade ! . getMakerDepositTxId ( ) ) ;
if ( ! ctx . taker . depositTx ) throw new Error ( "Taker deposit tx not found with id " + ctx . arbitrator . trade ! . getTakerDepositTxId ( ) ) ;
}
2024-01-07 16:15:48 -05:00
// record context
ctx . tradeAmount = BigInt ( trade . getAmount ( ) ) ; // re-assign trade amount which could be adjusted
ctx . maker . trade = await ctx . maker . havenod ! . getTrade ( ctx . offerId ! ) ;
ctx . taker . trade = await ctx . taker . havenod ! . getTrade ( ctx . offerId ! ) ;
ctx . maker . balancesAfterTake = await ctx . maker . havenod ! . getBalances ( ) ;
ctx . taker . balancesAfterTake = await ctx . taker . havenod ! . getBalances ( ) ;
2023-10-28 10:20:56 -04:00
ctx . maker . depositTxFee = BigInt ( ctx . maker . depositTx ! . getFee ( ) ) ;
ctx . taker . depositTxFee = BigInt ( ctx . taker . depositTx ! . getFee ( ) ) ;
2024-04-07 08:13:09 -04:00
ctx . maker . tradeFee = BigInt ( trade . getMakerFee ( ) ) ;
2023-10-28 10:20:56 -04:00
ctx . taker . tradeFee = BigInt ( trade . getTakerFee ( ) ) ;
ctx . getBuyer ( ) . securityDepositActual = BigInt ( trade . getBuyerSecurityDeposit ( ) ! ) ;
ctx . getSeller ( ) . securityDepositActual = BigInt ( trade . getSellerSecurityDeposit ( ) ! ) ;
}
2023-10-30 17:11:33 -04:00
// test trade model
await testTrade ( trade , ctx ) ;
2023-10-28 10:20:56 -04:00
// test buyer and seller balances after offer taken
if ( ! ctx . concurrentTrades ) {
ctx . arbitrator ! . trade = await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) ;
// test buyer balances after offer taken
const buyerBalanceDiff = BigInt ( ctx . getBuyer ( ) . balancesAfterTake ! . getBalance ( ) ) - BigInt ( ctx . getBuyer ( ) . balancesBeforeTake ! . getBalance ( ) ) ;
const buyerBalanceDiffReservedTrade = BigInt ( ctx . getBuyer ( ) . balancesAfterTake ! . getReservedTradeBalance ( ) ) - BigInt ( ctx . getBuyer ( ) . balancesBeforeTake ! . getReservedTradeBalance ( ) ) ;
const buyerBalanceDiffReservedOffer = BigInt ( ctx . getBuyer ( ) . balancesAfterTake ! . getReservedOfferBalance ( ) ) - BigInt ( ctx . getBuyer ( ) . balancesBeforeTake ! . getReservedOfferBalance ( ) ) ;
expect ( buyerBalanceDiffReservedTrade ) . toEqual ( BigInt ( trade . getBuyerSecurityDeposit ( ) ! ) ) ;
2024-08-29 11:32:14 -04:00
expect ( buyerBalanceDiff ) . toEqual ( - 1 n * buyerBalanceDiffReservedOffer - buyerBalanceDiffReservedTrade - ctx . getBuyer ( ) . depositTxFee ! - ctx . getBuyer ( ) . tradeFee ! ) ;
2023-10-28 10:20:56 -04:00
// test seller balances after offer taken
const sellerBalanceDiff = BigInt ( ctx . getSeller ( ) . balancesAfterTake ! . getBalance ( ) ) - BigInt ( ctx . getSeller ( ) . balancesBeforeTake ! . getBalance ( ) ) ;
const sellerBalanceDiffReservedTrade = BigInt ( ctx . getSeller ( ) . balancesAfterTake ! . getReservedTradeBalance ( ) ) - BigInt ( ctx . getSeller ( ) . balancesBeforeTake ! . getReservedTradeBalance ( ) ) ;
expect ( sellerBalanceDiffReservedTrade ) . toEqual ( BigInt ( trade . getAmount ( ) ) + BigInt ( trade . getSellerSecurityDeposit ( ) ! ) ) ;
2024-08-29 11:32:14 -04:00
expect ( sellerBalanceDiff ) . toEqual ( 0 n - ctx . getSeller ( ) . depositTxFee ! - ctx . getSeller ( ) . tradeFee ! - ctx . getSeller ( ) . securityDepositActual ! - ctx . tradeAmount ! ) ;
2023-10-28 10:20:56 -04:00
// test maker balances after offer taken
const makerBalanceDiffReservedOffer = BigInt ( ctx . getMaker ( ) . balancesAfterTake ! . getReservedOfferBalance ( ) ) - BigInt ( ctx . getMaker ( ) . balancesBeforeTake ! . getReservedOfferBalance ( ) ) ;
expect ( makerBalanceDiffReservedOffer ) . toBeLessThan ( 0 n ) ; // TODO: more precise?
// test taker balances after offer taken
const takerBalanceDiffReservedOffer = BigInt ( ctx . getTaker ( ) . balancesAfterTake ! . getReservedOfferBalance ( ) ) - BigInt ( ctx . getTaker ( ) . balancesBeforeTake ! . getReservedOfferBalance ( ) ) ;
expect ( takerBalanceDiffReservedOffer ) . toEqual ( 0 n ) ;
}
2024-07-17 17:41:47 -04:00
// test getting trade for all parties
await testGetTrade ( ctx ) ;
2022-12-13 08:44:20 +00:00
2023-10-28 10:20:56 -04:00
// market-priced offer amounts are unadjusted, fixed-priced offer amounts are adjusted (e.g. cash at atm is $10 increments)
// TODO: adjustments are based on payment method, not fixed-price
2024-07-17 17:41:47 -04:00
if ( trade . getOffer ( ) ! . getUseMarketBasedPrice ( ) ) {
assert . equal ( ctx . tradeAmount , BigInt ( trade . getAmount ( ) ) ) ;
2023-09-09 10:25:52 -04:00
} else {
2024-07-17 17:41:47 -04:00
expect ( Math . abs ( HavenoUtils . percentageDiff ( ctx . tradeAmount ! , BigInt ( trade . getAmount ( ) ) ) ) ) . toBeLessThan ( TestConfig . maxAdjustmentPct ) ;
2023-09-09 10:25:52 -04:00
}
2022-12-13 08:44:20 +00:00
2024-07-17 17:41:47 -04:00
// maker is notified of balance change
2022-10-01 07:47:46 -04:00
// taker is notified of balance change
return trade ;
}
2024-07-17 17:41:47 -04:00
async function testTrade ( trade : TradeInfo , ctx : TradeContext , havenod? : HavenoClient ) : Promise < void > {
2023-10-30 17:11:33 -04:00
expect ( BigInt ( trade . getAmount ( ) ) ) . toEqual ( ctx ! . tradeAmount ) ;
// test security deposit = max(.1, trade amount * security deposit pct)
2024-04-07 08:13:09 -04:00
const expectedSecurityDeposit = HavenoUtils . max ( HavenoUtils . xmrToAtomicUnits ( . 1 ) , HavenoUtils . multiply ( ctx . tradeAmount ! , ctx . securityDepositPct ! ) ) ;
2024-08-29 11:32:14 -04:00
expect ( BigInt ( trade . getBuyerSecurityDeposit ( ) ) ) . toEqual ( expectedSecurityDeposit - ctx . getBuyer ( ) . depositTxFee ! ) ;
expect ( BigInt ( trade . getSellerSecurityDeposit ( ) ) ) . toEqual ( expectedSecurityDeposit - ctx . getSeller ( ) . depositTxFee ! ) ;
2023-10-30 17:11:33 -04:00
2024-07-17 17:41:47 -04:00
// test phase
if ( ! ctx . isPaymentSent ) {
assert ( moneroTs . GenUtils . arrayContains ( [ "DEPOSITS_PUBLISHED" , "DEPOSITS_CONFIRMED" , "DEPOSITS_UNLOCKED" ] , trade . getPhase ( ) ) , "Unexpected trade phase: " + trade . getPhase ( ) ) ;
}
// test role
const role = trade . getRole ( ) ;
assert ( role . length > 0 ) ; // TODO: test role string based on context
2023-10-30 17:11:33 -04:00
// TODO: test more fields
2023-10-28 10:20:56 -04:00
}
2024-07-17 17:41:47 -04:00
async function testGetTrade ( ctx : TradeContext , havenod? : HavenoClient ) : Promise < void > {
if ( havenod ) {
const trade = await havenod . getTrade ( ctx . offerId ! ) ;
await testTrade ( trade , ctx ) ;
const trades = await havenod . getTrades ( ) ;
const foundTrade = trades . find ( ( trade ) = > trade . getTradeId ( ) === ctx . offerId ) ;
assert ( foundTrade ) ;
await testTrade ( foundTrade , ctx , havenod ) ;
} else {
await testGetTrade ( ctx , ctx . maker . havenod ) ;
await testGetTrade ( ctx , ctx . taker . havenod ) ;
await testGetTrade ( ctx , ctx . arbitrator . havenod ) ;
}
}
2023-10-28 10:20:56 -04:00
async function testOpenDispute ( ctxP : Partial < TradeContext > ) {
let ctx = TradeContext . init ( ctxP ) ;
2022-12-13 08:44:20 +00:00
2023-01-23 16:17:00 -05:00
// TODO: test open dispute when buyer or seller offline
2023-10-28 10:20:56 -04:00
if ( ! ctx . getBuyer ( ) . havenod || ! ctx . getSeller ( ) . havenod ) {
2023-01-23 16:17:00 -05:00
HavenoUtils . log ( 0 , "WARNING: skipping test open dispute tests because a trader is offline" ) ; // TODO: update tests for offline trader
return ;
}
2022-10-01 07:47:46 -04:00
// test dispute state
2023-10-28 10:20:56 -04:00
const openerDispute = await ctx . getDisputeOpener ( ) ! . havenod ! . getDispute ( ctx . offerId ! ) ;
2022-11-04 15:57:42 -04:00
expect ( openerDispute . getTradeId ( ) ) . toEqual ( ctx . offerId ) ;
2022-10-01 07:47:46 -04:00
expect ( openerDispute . getIsOpener ( ) ) . toBe ( true ) ;
2023-10-28 10:20:56 -04:00
expect ( openerDispute . getDisputeOpenerIsBuyer ( ) ) . toBe ( ctx . getDisputeOpener ( ) ! . havenod === ctx . getBuyer ( ) . havenod ) ;
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// get non-existing dispute should fail
try {
2023-10-28 10:20:56 -04:00
await ctx . getDisputeOpener ( ) ! . havenod ! . getDispute ( "invalid" ) ;
2022-10-01 07:47:46 -04:00
throw new Error ( "get dispute with invalid id should fail" ) ;
} catch ( err : any ) {
assert . equal ( err . message , "dispute for trade id 'invalid' not found" ) ;
}
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// peer sees the dispute
2023-10-28 10:20:56 -04:00
await wait ( ctx . maxTimePeerNoticeMs + TestConfig . maxWalletStartupMs ) ;
const peerDispute = await ctx . getDisputePeer ( ) ! . havenod ! . getDispute ( ctx . offerId ! ) ;
2022-11-04 15:57:42 -04:00
expect ( peerDispute . getTradeId ( ) ) . toEqual ( ctx . offerId ) ;
expect ( peerDispute . getIsOpener ( ) ) . toBe ( false || ctx . buyerDisputeContext === ctx . sellerDisputeContext ) ; // TODO: both peers think they're the opener if disputes opened at same time since not waiting for ack
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// arbitrator sees both disputes
2023-10-28 10:20:56 -04:00
const disputes = await ctx . arbitrator . havenod ! . getDisputes ( ) ;
2022-10-01 07:47:46 -04:00
expect ( disputes . length ) . toBeGreaterThanOrEqual ( 2 ) ;
const arbDisputePeer = disputes . find ( d = > d . getId ( ) === peerDispute . getId ( ) ) ;
assert ( arbDisputePeer ) ;
const arbDisputeOpener = disputes . find ( d = > d . getId ( ) === openerDispute . getId ( ) ) ;
assert ( arbDisputeOpener ) ;
2022-11-23 09:41:48 +00:00
// arbitrator has seller's payment account info
let sellerPaymentAccountPayload = arbDisputeOpener . getContract ( ) ! . getIsBuyerMakerAndSellerTaker ( ) ? arbDisputeOpener . getTakerPaymentAccountPayload ( ) : arbDisputeOpener . getMakerPaymentAccountPayload ( ) ;
2023-10-28 10:20:56 -04:00
let expectedSellerPaymentAccountPayload = ( await ctx . getSeller ( ) . havenod ? . getPaymentAccount ( sellerPaymentAccountPayload ? . getId ( ) ! ) ) ? . getPaymentAccountPayload ( ) ;
2022-11-23 09:41:48 +00:00
expect ( sellerPaymentAccountPayload ) . toEqual ( expectedSellerPaymentAccountPayload ) ;
2023-10-28 10:20:56 -04:00
expect ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( sellerPaymentAccountPayload ! ) ) . toEqual ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( expectedSellerPaymentAccountPayload ! ) ) ;
2022-11-23 09:41:48 +00:00
sellerPaymentAccountPayload = arbDisputePeer . getContract ( ) ! . getIsBuyerMakerAndSellerTaker ( ) ? arbDisputePeer . getTakerPaymentAccountPayload ( ) : arbDisputeOpener . getMakerPaymentAccountPayload ( ) ;
expect ( sellerPaymentAccountPayload ) . toEqual ( expectedSellerPaymentAccountPayload ) ;
2023-10-28 10:20:56 -04:00
expect ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( sellerPaymentAccountPayload ! ) ) . toEqual ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( expectedSellerPaymentAccountPayload ! ) ) ;
2022-11-23 09:41:48 +00:00
// arbitrator has buyer's payment account info unless opener is seller and payment not sent
let buyerPaymentAccountPayload = arbDisputeOpener . getContract ( ) ! . getIsBuyerMakerAndSellerTaker ( ) ? arbDisputeOpener . getMakerPaymentAccountPayload ( ) : arbDisputeOpener . getTakerPaymentAccountPayload ( ) ;
2023-10-28 10:20:56 -04:00
if ( ctx . getDisputeOpener ( ) ! . havenod === ctx . getSeller ( ) . havenod && ! ctx . isPaymentSent ) expect ( buyerPaymentAccountPayload ) . toBeUndefined ( ) ;
2022-11-23 09:41:48 +00:00
else {
2023-10-28 10:20:56 -04:00
let expectedBuyerPaymentAccountPayload = ( await ctx . getBuyer ( ) . havenod ? . getPaymentAccount ( buyerPaymentAccountPayload ? . getId ( ) ! ) ) ? . getPaymentAccountPayload ( ) ;
2022-11-23 09:41:48 +00:00
expect ( buyerPaymentAccountPayload ) . toEqual ( expectedBuyerPaymentAccountPayload ) ;
2023-10-28 10:20:56 -04:00
expect ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( buyerPaymentAccountPayload ! ) ) . toEqual ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( expectedBuyerPaymentAccountPayload ! ) ) ;
2022-11-23 09:41:48 +00:00
}
buyerPaymentAccountPayload = arbDisputePeer . getContract ( ) ! . getIsBuyerMakerAndSellerTaker ( ) ? arbDisputePeer . getMakerPaymentAccountPayload ( ) : arbDisputePeer . getTakerPaymentAccountPayload ( ) ;
2023-10-28 10:20:56 -04:00
if ( ctx . getDisputeOpener ( ) ! . havenod === ctx . getSeller ( ) . havenod && ! ctx . isPaymentSent ) expect ( buyerPaymentAccountPayload ) . toBeUndefined ( ) ;
2022-11-23 09:41:48 +00:00
else {
2023-10-28 10:20:56 -04:00
let expectedBuyerPaymentAccountPayload = ( await ctx . getBuyer ( ) . havenod ? . getPaymentAccount ( buyerPaymentAccountPayload ? . getId ( ) ! ) ) ? . getPaymentAccountPayload ( ) ;
2022-11-23 09:41:48 +00:00
expect ( buyerPaymentAccountPayload ) . toEqual ( expectedBuyerPaymentAccountPayload ) ;
2023-10-28 10:20:56 -04:00
expect ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( buyerPaymentAccountPayload ! ) ) . toEqual ( await ctx . arbitrator . havenod ? . getPaymentAccountPayloadForm ( expectedBuyerPaymentAccountPayload ! ) ) ;
2022-11-23 09:41:48 +00:00
}
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// register to receive notifications
const disputeOpenerNotifications : NotificationMessage [ ] = [ ] ;
const disputePeerNotifications : NotificationMessage [ ] = [ ] ;
const arbitratorNotifications : NotificationMessage [ ] = [ ] ;
2023-10-28 10:20:56 -04:00
await ctx . getDisputeOpener ( ) ! . havenod ! . addNotificationListener ( notification = > { HavenoUtils . log ( 3 , "Dispute opener received notification " + notification . getType ( ) + " " + ( notification . getChatMessage ( ) ? notification . getChatMessage ( ) ? . getMessage ( ) : "" ) ) ; disputeOpenerNotifications . push ( notification ) ; } ) ;
await ctx . getDisputePeer ( ) ! . havenod ! . addNotificationListener ( notification = > { HavenoUtils . log ( 3 , "Dispute peer received notification " + notification . getType ( ) + " " + ( notification . getChatMessage ( ) ? notification . getChatMessage ( ) ? . getMessage ( ) : "" ) ) ; disputePeerNotifications . push ( notification ) ; } ) ;
2022-10-01 07:47:46 -04:00
await arbitrator . addNotificationListener ( notification = > { HavenoUtils . log ( 3 , "Arbitrator received notification " + notification . getType ( ) + " " + ( notification . getChatMessage ( ) ? notification . getChatMessage ( ) ? . getMessage ( ) : "" ) ) ; arbitratorNotifications . push ( notification ) ; } ) ;
2022-12-13 08:44:20 +00:00
2024-04-26 15:00:01 -04:00
// test chat messages
if ( ctx . testChatMessages ) {
// arbitrator sends chat messages to traders
HavenoUtils . log ( 1 , "Arbitrator sending chat messages to traders. tradeId=" + ctx . offerId + ", disputeId=" + openerDispute . getId ( ) ) ;
await ctx . arbitrator . havenod ! . sendDisputeChatMessage ( arbDisputeOpener ! . getId ( ) , "Arbitrator chat message to dispute opener" , [ ] ) ;
await ctx . arbitrator . havenod ! . sendDisputeChatMessage ( arbDisputePeer ! . getId ( ) , "Arbitrator chat message to dispute peer" , [ ] ) ;
// traders reply to arbitrator chat messages
await wait ( ctx . maxTimePeerNoticeMs ) ; // wait for arbitrator's message to arrive
const attachment = new Attachment ( ) ;
const bytes = new Uint8Array ( Buffer . from ( "Proof dispute opener was scammed" , "utf8" ) ) ;
attachment . setBytes ( bytes ) ;
attachment . setFileName ( "proof.txt" ) ;
const attachment2 = new Attachment ( ) ;
const bytes2 = new Uint8Array ( Buffer . from ( "picture bytes" , "utf8" ) ) ;
attachment2 . setBytes ( bytes2 ) ;
attachment2 . setFileName ( "proof.png" ) ;
HavenoUtils . log ( 2 , "Dispute opener sending chat message to arbitrator. tradeId=" + ctx . offerId + ", disputeId=" + openerDispute . getId ( ) ) ;
await ctx . getDisputeOpener ( ) ! . havenod ! . sendDisputeChatMessage ( openerDispute . getId ( ) , "Dispute opener chat message" , [ attachment , attachment2 ] ) ;
await wait ( ctx . maxTimePeerNoticeMs ) ; // wait for user2's message to arrive
HavenoUtils . log ( 2 , "Dispute peer sending chat message to arbitrator. tradeId=" + ctx . offerId + ", disputeId=" + peerDispute . getId ( ) ) ;
await ctx . getDisputePeer ( ) ! . havenod ! . sendDisputeChatMessage ( peerDispute . getId ( ) , "Dispute peer chat message" , [ ] ) ;
// test trader chat messages
await wait ( ctx . maxTimePeerNoticeMs ) ;
let dispute = await ctx . getDisputeOpener ( ) ! . havenod ! . getDispute ( ctx . offerId ! ) ;
let messages = dispute . getChatMessageList ( ) ;
expect ( messages . length ) . toBeGreaterThanOrEqual ( 3 ) ; // last messages are chat, first messages are system message and possibly DisputeOpenedMessage acks
2024-08-06 05:10:08 -04:00
try {
expect ( messages [ messages . length - 2 ] . getMessage ( ) ) . toEqual ( "Arbitrator chat message to dispute opener" ) ;
expect ( messages [ messages . length - 1 ] . getMessage ( ) ) . toEqual ( "Dispute opener chat message" ) ;
} catch ( err ) {
console . log ( "Dispute peer chat messages length: " + messages . length ) ;
console . log ( "Dispute peer chat messages : " + JSON . stringify ( messages ) ) ;
throw err ;
}
2024-04-26 15:00:01 -04:00
let attachments = messages [ messages . length - 1 ] . 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 ) ;
dispute = await ctx . getDisputePeer ( ) ! . havenod ! . getDispute ( ctx . offerId ! ) ;
messages = dispute . getChatMessageList ( ) ;
expect ( messages . length ) . toBeGreaterThanOrEqual ( 3 ) ;
2024-08-06 05:10:08 -04:00
try {
expect ( messages [ messages . length - 2 ] . getMessage ( ) ) . toEqual ( "Arbitrator chat message to dispute peer" ) ;
expect ( messages [ messages . length - 1 ] . getMessage ( ) ) . toEqual ( "Dispute peer chat message" ) ;
} catch ( err ) {
console . log ( "Dispute peer chat messages length: " + messages . length ) ;
console . log ( "Dispute peer chat messages : " + JSON . stringify ( messages ) ) ;
throw err ;
}
2024-04-26 15:00:01 -04:00
// test notifications of chat messages
let chatNotifications = getNotifications ( disputeOpenerNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE , ctx . offerId ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( "Arbitrator chat message to dispute opener" ) ;
chatNotifications = getNotifications ( disputePeerNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE , ctx . offerId ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( "Arbitrator chat message to dispute peer" ) ;
// arbitrator has 2 chat messages, one with attachments
chatNotifications = getNotifications ( arbitratorNotifications , NotificationMessage . NotificationType . CHAT_MESSAGE , ctx . offerId ) ;
expect ( chatNotifications . length ) . toBe ( 2 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( "Dispute opener 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 ( "Dispute peer chat message" ) ;
}
2022-10-01 07:47:46 -04:00
}
2023-10-28 10:20:56 -04:00
async function resolveDispute ( ctxP : Partial < TradeContext > ) {
let ctx = TradeContext . init ( ctxP ) ;
2022-10-01 07:47:46 -04:00
2023-01-23 16:17:00 -05:00
// stop buyer or seller depending on configuration
const promises : Promise < void > [ ] = [ ] ;
2023-10-28 10:20:56 -04:00
if ( ctx . getBuyer ( ) . havenod && ctx . buyerOfflineAfterDisputeOpened ) {
promises . push ( releaseHavenoProcess ( ctx . getBuyer ( ) . havenod ! ) ) ; // stop buyer
if ( ctx . isBuyerMaker ( ) ) ctx . maker . havenod = undefined ;
else ctx . taker . havenod = undefined ;
2023-01-23 16:17:00 -05:00
}
2023-10-28 10:20:56 -04:00
if ( ctx . getSeller ( ) . havenod && ctx . sellerOfflineAfterDisputeOpened ) {
promises . push ( releaseHavenoProcess ( ctx . getSeller ( ) . havenod ! ) ) ; // stop seller
if ( ctx . isBuyerMaker ( ) ) ctx . taker . havenod = undefined ;
else ctx . maker . havenod = undefined ;
2023-01-23 16:17:00 -05:00
}
await Promise . all ( promises ) ;
2023-10-28 10:20:56 -04:00
// award too much to winner (majority receiver)
let trade = await arbitrator . getTrade ( ctx . offerId ! )
const tradeAmount : bigint = BigInt ( trade ! . getAmount ( ) ) ;
let customWinnerAmount = tradeAmount + BigInt ( trade . getBuyerSecurityDeposit ( ) ) + BigInt ( trade . getSellerSecurityDeposit ( ) + 1 n ) ; // mining fee is subtracted from security deposits
2023-03-11 08:33:11 -05:00
try {
await arbitrator . resolveDispute ( ctx . offerId ! , ctx . disputeWinner ! , ctx . disputeReason ! , "Winner gets too much" , customWinnerAmount ) ;
throw new Error ( "Should have failed resolving dispute with too much winner payout" ) ;
} catch ( err : any ) {
assert . equal ( err . message , "Winner payout is more than the trade wallet's balance" ) ;
}
2023-01-23 16:17:00 -05:00
2023-10-28 10:20:56 -04:00
// award too little to loser (minority receiver)
2023-07-14 08:44:48 -04:00
let makerDepositTx = await monerod . getTx ( trade . getMakerDepositTxId ( ) ) ;
let takerDepositTx = await monerod . getTx ( trade . getTakerDepositTxId ( ) ) ;
2023-12-27 16:58:30 -05:00
customWinnerAmount = tradeAmount + BigInt ( trade . getBuyerSecurityDeposit ( ) ) + BigInt ( trade . getSellerSecurityDeposit ( ) ) - 10000 n ;
2022-10-01 07:47:46 -04:00
try {
2022-11-04 15:57:42 -04:00
await arbitrator . resolveDispute ( ctx . offerId ! , ctx . disputeWinner ! , ctx . disputeReason ! , "Loser gets too little" , customWinnerAmount ) ;
2022-10-01 07:47:46 -04:00
throw new Error ( "Should have failed resolving dispute with insufficient loser payout" ) ;
} catch ( err : any ) {
assert . equal ( err . message , "Loser payout is too small to cover the mining fee" ) ;
}
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// resolve dispute according to configuration
2022-11-04 15:57:42 -04:00
HavenoUtils . log ( 1 , "Resolving dispute for trade " + ctx . offerId ) ;
2022-10-01 07:47:46 -04:00
const startTime = Date . now ( ) ;
2022-11-04 15:57:42 -04:00
await arbitrator . resolveDispute ( ctx . offerId ! , ctx . disputeWinner ! , ctx . disputeReason ! , ctx . disputeSummary ! , ctx . disputeWinnerAmount ) ;
2022-10-01 07:47:46 -04:00
HavenoUtils . log ( 1 , "Done resolving dispute (" + ( Date . now ( ) - startTime ) + ")" ) ;
2022-12-13 08:44:20 +00:00
2023-01-23 16:17:00 -05:00
// start buyer or seller depending on configuration
2023-10-28 10:20:56 -04:00
if ( ! ctx . getBuyer ( ) . havenod && ctx . buyerOfflineAfterDisputeOpened === false ) {
// TODO: wait additional time before starting to avoid 503? need to wait after shut down?
2023-01-23 16:17:00 -05:00
const buyer = await initHaveno ( { appName : ctx.buyerAppName , excludePorts : ctx.usedPorts } ) ; // start buyer
2023-10-28 10:20:56 -04:00
if ( ctx . isBuyerMaker ( ) ) ctx . maker . havenod = buyer ;
else ctx . taker . havenod = buyer ;
2023-01-23 16:17:00 -05:00
ctx . usedPorts ! . push ( getPort ( buyer . getUrl ( ) ) ) ;
}
2023-10-28 10:20:56 -04:00
if ( ! ctx . getSeller ( ) . havenod && ctx . sellerOfflineAfterDisputeOpened === false ) {
2023-01-23 16:17:00 -05:00
const seller = await initHaveno ( { appName : ctx.sellerAppName , excludePorts : ctx.usedPorts } ) ; // start seller
2023-10-28 10:20:56 -04:00
if ( ctx . isBuyerMaker ( ) ) ctx . taker . havenod = seller ;
else ctx . maker . havenod = seller ;
ctx . usedPorts ! . push ( getPort ( ctx . getSeller ( ) . havenod ! . getUrl ( ) ) )
2023-01-23 16:17:00 -05:00
}
2022-10-01 07:47:46 -04:00
// test resolved dispute
2023-10-28 10:20:56 -04:00
await wait ( TestConfig . maxWalletStartupMs + ctx . walletSyncPeriodMs * 2 ) ;
if ( ctx . getDisputeOpener ( ) ! . havenod ) {
const dispute = await ctx . getDisputeOpener ( ) ! . havenod ! . getDispute ( ctx . offerId ! ) ;
2023-01-23 16:17:00 -05:00
assert ( dispute . getIsClosed ( ) , "Dispute is not closed for opener, trade " + ctx . offerId ) ;
}
2023-10-28 10:20:56 -04:00
if ( ctx . getDisputePeer ( ) ! . havenod ) {
const dispute = await ctx . getDisputePeer ( ) ! . havenod ! . getDispute ( ctx . offerId ! ) ;
2023-04-17 10:13:56 -04:00
assert ( dispute . getIsClosed ( ) , "Dispute is not closed for opener's peer, trade " + ctx . offerId ) ;
2023-01-23 16:17:00 -05:00
}
2022-12-13 08:44:20 +00:00
2022-11-04 15:57:42 -04:00
// test trade state
2023-12-15 09:24:23 -05:00
if ( ctx . getBuyer ( ) . havenod ) await testTradeState ( await ctx . getBuyer ( ) . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , disputeState : "DISPUTE_CLOSED" , isCompleted : true , isPayoutPublished : true } ) ;
if ( ctx . getSeller ( ) . havenod ) await testTradeState ( await ctx . getSeller ( ) . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , disputeState : "DISPUTE_CLOSED" , isCompleted : true , isPayoutPublished : true } ) ;
await testTradeState ( await ctx . arbitrator . havenod ! . getTrade ( ctx . offerId ! ) , { phase : ctx.getPhase ( ) , payoutState : [ "PAYOUT_PUBLISHED" , "PAYOUT_CONFIRMED" , "PAYOUT_UNLOCKED" ] , disputeState : "DISPUTE_CLOSED" , isCompleted : true , isPayoutPublished : true } ) ;
2023-10-28 10:20:56 -04:00
// signing peer has payout tx id on 0 conf (peers must wait for confirmation to see outgoing tx)
2023-12-07 18:05:15 -05:00
const winnerd = ctx . disputeWinner === DisputeResult . Winner . BUYER ? ctx . getBuyer ( ) . havenod : ctx.getSeller ( ) . havenod ;
const loserd = ctx . disputeWinner === DisputeResult . Winner . BUYER ? ctx . getSeller ( ) . havenod : ctx.getBuyer ( ) . havenod ;
const signerd = winnerd ? winnerd : loserd ;
ctx . payoutTxId = ( await signerd ! . getTrade ( ctx . offerId ! ) ) . getPayoutTxId ( ) ;
2023-10-28 10:20:56 -04:00
// record balances on completion
if ( ! ctx . maker . balancesAfterPayout ) {
ctx . maker . balancesAfterPayout = await ctx . maker . havenod ? . getBalances ( ) ;
ctx . taker . balancesAfterPayout = await ctx . taker . havenod ? . getBalances ( ) ;
}
2022-11-04 15:57:42 -04:00
// test balances after payout tx unless concurrent trades
2023-12-07 18:05:15 -05:00
if ( ! ctx . concurrentTrades ) await testAmountsAfterComplete ( ctx ) ;
2022-11-04 15:57:42 -04:00
// test payout unlock
await testTradePayoutUnlock ( ctx ) ;
2022-10-01 07:47:46 -04:00
}
2023-12-07 18:05:15 -05:00
async function testAmountsAfterComplete ( tradeCtx : TradeContext ) {
2023-10-28 10:20:56 -04:00
2023-12-07 18:05:15 -05:00
// get payout tx
2023-10-28 10:20:56 -04:00
if ( ! tradeCtx . payoutTxId ) throw new Error ( "Missing payout tx id" ) ;
const payoutTx = await monerod . getTx ( tradeCtx . payoutTxId ) ;
const payoutTxFee = BigInt ( payoutTx ! . getFee ( ) ) ;
2023-12-07 18:05:15 -05:00
// get expected payouts for normal trade
const isDisputedTrade = tradeCtx . getDisputeOpener ( ) !== undefined ;
if ( ! isDisputedTrade ) {
tradeCtx . getBuyer ( ) . payoutTxFee = payoutTxFee / 2 n ;
2024-08-29 11:32:14 -04:00
tradeCtx . getBuyer ( ) . payoutAmount = tradeCtx . getBuyer ( ) . securityDepositActual ! + tradeCtx . tradeAmount ! - tradeCtx . getBuyer ( ) . payoutTxFee ! ;
2023-12-07 18:05:15 -05:00
tradeCtx . getSeller ( ) . payoutTxFee = payoutTxFee / 2 n ;
2024-08-29 11:32:14 -04:00
tradeCtx . getSeller ( ) . payoutAmount = tradeCtx . getSeller ( ) . securityDepositActual ! - tradeCtx . getSeller ( ) . payoutTxFee ! ;
2023-12-07 18:05:15 -05:00
} else {
2023-10-28 10:20:56 -04:00
2023-12-07 18:05:15 -05:00
// get expected payouts for disputed trade
2023-10-28 10:20:56 -04:00
const winnerGetsAll = tradeCtx . disputeWinnerAmount === tradeCtx . maker . securityDepositActual ! + tradeCtx . taker . securityDepositActual ! + tradeCtx . tradeAmount ! ;
2023-12-07 18:05:15 -05:00
if ( tradeCtx . disputeWinnerAmount ) {
tradeCtx . getDisputeWinner ( ) ! . payoutTxFee = winnerGetsAll ? payoutTxFee : 0n ;
2024-08-29 11:32:14 -04:00
tradeCtx . getDisputeWinner ( ) ! . payoutAmount = tradeCtx . disputeWinnerAmount - tradeCtx . getDisputeWinner ( ) ! . payoutTxFee ! ;
2023-12-07 18:05:15 -05:00
tradeCtx . getDisputeLoser ( ) ! . payoutTxFee = winnerGetsAll ? 0n : payoutTxFee ;
2024-08-29 11:32:14 -04:00
tradeCtx . getDisputeLoser ( ) ! . payoutAmount = tradeCtx . maker . securityDepositActual ! + tradeCtx . taker . securityDepositActual ! + tradeCtx . tradeAmount ! - tradeCtx . disputeWinnerAmount - tradeCtx . getDisputeLoser ( ) ! . payoutTxFee ! ;
2023-10-28 10:20:56 -04:00
} else {
2023-12-07 18:05:15 -05:00
tradeCtx . getDisputeWinner ( ) ! . payoutTxFee = payoutTxFee / 2 n ;
2024-08-29 11:32:14 -04:00
tradeCtx . getDisputeWinner ( ) ! . payoutAmount = tradeCtx . tradeAmount ! + tradeCtx . getDisputeWinner ( ) ! . securityDepositActual ! - tradeCtx . getDisputeWinner ( ) ! . payoutTxFee ! ;
2023-12-07 18:05:15 -05:00
tradeCtx . getDisputeLoser ( ) ! . payoutTxFee = payoutTxFee / 2 n ;
2024-08-29 11:32:14 -04:00
tradeCtx . getDisputeLoser ( ) ! . payoutAmount = tradeCtx . getDisputeLoser ( ) ! . securityDepositActual ! - tradeCtx . getDisputeLoser ( ) ! . payoutTxFee ! ;
2023-10-28 10:20:56 -04:00
}
}
2023-12-07 18:05:15 -05:00
// TODO: payout tx is unknown to offline non-signer until confirmed
if ( isDisputedTrade || tradeCtx . isOfflineFlow ( ) ) {
await mineToHeight ( await monerod . getHeight ( ) + 1 ) ;
await wait ( TestConfig . maxWalletStartupMs + tradeCtx . walletSyncPeriodMs * 2 ) ;
}
// test trade payouts
if ( tradeCtx . maker . havenod ) await testPeerAmountsAfterComplete ( tradeCtx , tradeCtx . getMaker ( ) ) ;
if ( tradeCtx . taker . havenod ) await testPeerAmountsAfterComplete ( tradeCtx , tradeCtx . getTaker ( ) ) ;
}
async function testPeerAmountsAfterComplete ( tradeCtx : TradeContext , peerCtx : PeerContext ) {
// get trade
2024-08-29 11:32:14 -04:00
const trade = await peerCtx . havenod ! . getTrade ( tradeCtx . offerId ! ) ;
2023-12-07 18:05:15 -05:00
// test trade amounts
const isBuyer = tradeCtx . getBuyer ( ) === peerCtx ;
if ( isBuyer ) expect ( BigInt ( trade . getBuyerDepositTxFee ( ) ) ) . toEqual ( tradeCtx . getBuyer ( ) . depositTxFee ) ; // TODO: get and test peer's security deposit tx fee?
else expect ( BigInt ( trade . getSellerDepositTxFee ( ) ) ) . toEqual ( tradeCtx . getSeller ( ) . depositTxFee ) ;
expect ( BigInt ( trade . getBuyerPayoutTxFee ( ) ) ) . toEqual ( tradeCtx . getBuyer ( ) . payoutTxFee ) ;
expect ( BigInt ( trade . getSellerPayoutTxFee ( ) ) ) . toEqual ( tradeCtx . getSeller ( ) . payoutTxFee ) ;
expect ( BigInt ( trade . getBuyerPayoutAmount ( ) ) ) . toEqual ( tradeCtx . getBuyer ( ) . payoutAmount ) ;
expect ( BigInt ( trade . getSellerPayoutAmount ( ) ) ) . toEqual ( tradeCtx . getSeller ( ) . payoutAmount ) ;
2023-10-28 10:20:56 -04:00
// test balance change after payout tx
const differenceAfterPayout = BigInt ( peerCtx . balancesAfterPayout ? . getBalance ( ) ! ) - BigInt ( peerCtx . balancesBeforePayout ? . getBalance ( ) ! ) ;
expect ( differenceAfterPayout ) . toEqual ( peerCtx . payoutAmount ) ;
// test balance change since before offer
if ( tradeCtx . testBalanceChangeEndToEnd ) {
// calculate expected balance from before offer
const sendTradeAmount = tradeCtx . getBuyer ( ) === peerCtx ? 0n : BigInt ( trade . getAmount ( ) ) ;
2024-08-29 11:32:14 -04:00
const expectedBalanceAfterComplete = BigInt ( peerCtx . balancesBeforeOffer ? . getBalance ( ) ! ) - peerCtx . splitOutputTxFee ! - peerCtx . tradeFee ! - sendTradeAmount - peerCtx . depositTxFee ! - peerCtx . securityDepositActual ! + peerCtx . payoutAmount ! ;
2023-10-28 10:20:56 -04:00
// log the math
HavenoUtils . log ( 1 , "Testing end-to-end balance change:" ) ;
HavenoUtils . log ( 1 , "Expected balance after = balance before - split output tx fee if maker and exact amount reserved - trade fee - trade amount if seller - deposit tx fee - security deposit received + (trade amount if seller + security deposit received - (payout tx fee / 2))" ) ;
HavenoUtils . log ( 1 , expectedBalanceAfterComplete + " = " + BigInt ( peerCtx . balancesBeforeOffer ? . getBalance ( ) ! ) + " - " + peerCtx . splitOutputTxFee + " - " + peerCtx . tradeFee ! + " - " + sendTradeAmount + " - " + peerCtx . depositTxFee + " - " + peerCtx . securityDepositActual + " + " + peerCtx . payoutAmount ) ;
// test the expected balance
expect ( BigInt ( peerCtx . balancesAfterPayout ? . getBalance ( ) ! ) ) . toEqual ( expectedBalanceAfterComplete ) ;
}
}
async function testTradeChat ( ctxP : Partial < TradeContext > ) {
const ctx = TradeContext . init ( ctxP ) ;
2022-10-01 07:47:46 -04:00
HavenoUtils . log ( 1 , "Testing trade chat" ) ;
// invalid trade should throw error
try {
await user1 . getChatMessages ( "invalid" ) ;
throw new Error ( "get chat messages with invalid id should fail" ) ;
} catch ( err : any ) {
assert . equal ( err . message , "trade with id 'invalid' not found" ) ;
}
// trade chat should be in initial state
2022-12-21 15:25:52 +00:00
let messages = await user1 . getChatMessages ( ctx . offerId ! ) ;
2022-10-01 07:47:46 -04:00
assert ( messages . length === 0 ) ;
2022-12-21 15:25:52 +00:00
messages = await user2 . getChatMessages ( ctx . offerId ! ) ;
2022-10-01 07:47:46 -04:00
assert ( messages . length === 0 ) ;
// add notification handlers and send some messages
const user1Notifications : NotificationMessage [ ] = [ ] ;
const user2Notifications : NotificationMessage [ ] = [ ] ;
await user1 . addNotificationListener ( notification = > { user1Notifications . push ( notification ) ; } ) ;
await user2 . addNotificationListener ( notification = > { user2Notifications . push ( notification ) ; } ) ;
// send simple conversation and verify the list of messages
const user1Msg = "Hi I'm user1" ;
2022-12-21 15:25:52 +00:00
await user1 . sendChatMessage ( ctx . offerId ! , user1Msg ) ;
2023-10-28 10:20:56 -04:00
await wait ( ctx . maxTimePeerNoticeMs ) ;
2022-12-21 15:25:52 +00:00
messages = await user2 . getChatMessages ( ctx . offerId ! ) ;
2022-10-01 07:47:46 -04:00
expect ( messages . length ) . toEqual ( 2 ) ;
expect ( messages [ 0 ] . getIsSystemMessage ( ) ) . toEqual ( true ) ; // first message is system
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( user1Msg ) ;
const user2Msg = "Hello I'm user2" ;
2022-12-21 15:25:52 +00:00
await user2 . sendChatMessage ( ctx . offerId ! , user2Msg ) ;
2023-10-28 10:20:56 -04:00
await wait ( ctx . maxTimePeerNoticeMs ) ;
2022-12-21 15:25:52 +00:00
messages = await user1 . getChatMessages ( ctx . offerId ! ) ;
2022-10-01 07:47:46 -04:00
expect ( messages . length ) . toEqual ( 3 ) ;
expect ( messages [ 0 ] . getIsSystemMessage ( ) ) . toEqual ( true ) ;
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( user1Msg ) ;
expect ( messages [ 2 ] . getMessage ( ) ) . toEqual ( user2Msg ) ;
// verify notifications
let chatNotifications = getNotifications ( user1Notifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( user2Msg ) ;
chatNotifications = getNotifications ( user2Notifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
expect ( chatNotifications . length ) . toBe ( 1 ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( user1Msg ) ;
// additional msgs
const msgs = [ "" , " " , "<script>alert('test');</script>" , "さようなら" ] ;
for ( const msg of msgs ) {
2022-12-21 15:25:52 +00:00
await user1 . sendChatMessage ( ctx . offerId ! , msg ) ;
2022-10-01 07:47:46 -04:00
await wait ( 1000 ) ; // the async operation can result in out of order messages
}
2023-10-28 10:20:56 -04:00
await wait ( ctx . maxTimePeerNoticeMs ) ;
2022-12-21 15:25:52 +00:00
messages = await user2 . getChatMessages ( ctx . offerId ! ) ;
2022-10-01 07:47:46 -04:00
let offset = 3 ; // 3 existing messages
expect ( messages . length ) . toEqual ( offset + msgs . length ) ;
expect ( messages [ 0 ] . getIsSystemMessage ( ) ) . toEqual ( true ) ;
expect ( messages [ 1 ] . getMessage ( ) ) . toEqual ( user1Msg ) ;
expect ( messages [ 2 ] . getMessage ( ) ) . toEqual ( user2Msg ) ;
for ( let i = 0 ; i < msgs . length ; i ++ ) {
expect ( messages [ i + offset ] . getMessage ( ) ) . toEqual ( msgs [ i ] ) ;
}
chatNotifications = getNotifications ( user2Notifications , NotificationMessage . NotificationType . CHAT_MESSAGE ) ;
offset = 1 ; // 1 existing notification
expect ( chatNotifications . length ) . toBe ( offset + msgs . length ) ;
expect ( chatNotifications [ 0 ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( user1Msg ) ;
for ( let i = 0 ; i < msgs . length ; i ++ ) {
expect ( chatNotifications [ i + offset ] . getChatMessage ( ) ? . getMessage ( ) ) . toEqual ( msgs [ i ] ) ;
}
}
// ---------------------------- OTHER HELPERS ---------------------------------
function getPort ( url : string ) : string {
return new URL ( url ) . port ;
}
function getBaseCurrencyNetwork ( ) : BaseCurrencyNetwork {
const str = getBaseCurrencyNetworkStr ( ) ;
if ( str === "XMR_MAINNET" ) return BaseCurrencyNetwork . XMR_MAINNET ;
else if ( str === "XMR_STAGENET" ) return BaseCurrencyNetwork . XMR_STAGENET ;
else if ( str === "XMR_LOCAL" ) return BaseCurrencyNetwork . XMR_LOCAL ;
else throw new Error ( "Unhandled base currency network: " + str ) ;
function getBaseCurrencyNetworkStr() {
for ( const arg of process . argv ) {
if ( arg . indexOf ( "--baseCurrencyNetwork" ) === 0 ) {
return arg . substring ( arg . indexOf ( "=" ) + 1 ) ;
}
}
throw new Error ( "Must provide base currency network, e.g.: `npm run test -- --baseCurrencyNetwork=XMR_LOCAL -t \"my test\"`" ) ;
}
}
function getNetworkStartPort() {
switch ( getBaseCurrencyNetwork ( ) ) {
case BaseCurrencyNetwork.XMR_MAINNET : return 1 ;
case BaseCurrencyNetwork.XMR_LOCAL : return 2 ;
case BaseCurrencyNetwork.XMR_STAGENET : return 3 ;
default : throw new Error ( "Unhandled base currency network: " + getBaseCurrencyNetwork ( ) ) ;
}
}
function getArbitratorPrivKey ( index : number ) {
const privKey = TestConfig . arbitratorPrivKeys [ getBaseCurrencyNetwork ( ) ] [ index ] ;
if ( ! privKey ) throw new Error ( "No arbitrator private key at index " + index ) ;
return privKey ;
}
async function initHavenos ( numDaemons : number , config? : any ) {
const havenodPromises : Promise < HavenoClient > [ ] = [ ] ;
for ( let i = 0 ; i < numDaemons ; i ++ ) havenodPromises . push ( initHaveno ( config ) ) ;
return Promise . all ( havenodPromises ) ;
}
2022-12-03 13:03:44 +00:00
async function initHaveno ( ctx? : HavenodContext ) : Promise < HavenoClient > {
if ( ! ctx ) ctx = { } ;
Object . assign ( ctx , TestConfig . defaultHavenod , Object . assign ( { } , ctx ) ) ;
2023-10-02 08:16:54 -04:00
if ( ! ctx . appName ) ctx . appName = "haveno-" + TestConfig . baseCurrencyNetwork + "_instance_" + moneroTs . GenUtils . getUUID ( ) ;
2022-12-03 13:03:44 +00:00
2022-10-01 07:47:46 -04:00
// connect to existing server or start new process
let havenod : HavenoClient ;
try {
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// try to connect to existing server
2022-12-03 13:03:44 +00:00
if ( ! ctx . port ) throw new Error ( "Cannot connect without port" ) ;
havenod = new HavenoClient ( "http://localhost:" + ctx . port , ctx . apiPassword ! ) ;
2022-10-01 07:47:46 -04:00
await havenod . getVersion ( ) ;
} catch ( err : any ) {
2022-12-13 08:44:20 +00:00
2022-10-01 07:47:46 -04:00
// get port for haveno process
2022-12-03 13:03:44 +00:00
if ( ! ctx . port ) {
for ( const httpPort of Array . from ( TestConfig . ports . keys ( ) ) ) {
if ( httpPort === "8079" || httpPort === "8080" || httpPort === "8081" ) continue ; // reserved for arbitrator, user1, and user2
2023-10-02 08:16:54 -04:00
if ( ! moneroTs . GenUtils . arrayContains ( HAVENO_PROCESS_PORTS , httpPort ) && ( ! ctx . excludePorts || ! moneroTs . GenUtils . arrayContains ( ctx . excludePorts , httpPort ) ) ) {
2022-12-03 13:03:44 +00:00
HAVENO_PROCESS_PORTS . push ( httpPort ) ;
ctx . port = httpPort ;
2022-02-09 01:41:00 -08:00
break ;
}
2021-12-16 20:10:40 -05:00
}
2021-12-08 06:22:36 -05:00
}
2022-12-03 13:03:44 +00:00
if ( ! ctx . port ) throw new Error ( "No unused test ports available" ) ;
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// start haveno process using configured ports if available
2022-05-01 13:30:11 -04:00
const cmd : string [ ] = [
2022-02-09 01:41:00 -08:00
"./haveno-daemon" ,
2022-07-07 09:11:50 -04:00
"--baseCurrencyNetwork" , TestConfig . baseCurrencyNetwork ,
"--useLocalhostForP2P" , TestConfig . baseCurrencyNetwork === BaseCurrencyNetwork . XMR_MAINNET ? "false" : "true" , // TODO: disable for stagenet too
"--useDevPrivilegeKeys" , TestConfig . baseCurrencyNetwork === BaseCurrencyNetwork . XMR_LOCAL ? "true" : "false" ,
2022-12-03 13:03:44 +00:00
"--nodePort" , TestConfig . ports . get ( ctx . port ) ! [ 1 ] ,
"--appName" , ctx . appName ,
2022-02-09 01:41:00 -08:00
"--apiPassword" , "apitest" ,
2022-12-03 13:03:44 +00:00
"--apiPort" , TestConfig . ports . get ( ctx . port ) ! [ 0 ] ,
"--walletRpcBindPort" , ctx . walletUrl ? getPort ( ctx . walletUrl ) : "" + await getAvailablePort ( ) , // use configured port if given
2023-01-18 10:11:47 -05:00
"--passwordRequired" , ( ctx . accountPasswordRequired ? "true" : "false" ) ,
"--logLevel" , ctx . logLevel !
2022-02-09 01:41:00 -08:00
] ;
2022-12-03 13:03:44 +00:00
havenod = await HavenoClient . startProcess ( TestConfig . haveno . path , cmd , "http://localhost:" + ctx . port , ctx . logProcessOutput ! ) ;
2022-02-09 01:41:00 -08:00
HAVENO_PROCESSES . push ( havenod ) ;
2023-01-10 08:30:47 -05:00
// wait to process network notifications
await wait ( 3000 ) ;
2021-12-08 06:22:36 -05:00
}
2022-12-13 08:44:20 +00:00
2022-02-09 01:41:00 -08:00
// open account if configured
2023-02-26 10:36:14 -05:00
if ( ctx . autoLogin ) {
try {
await initHavenoAccount ( havenod , ctx . accountPassword ! ) ;
} catch ( err ) {
await releaseHavenoProcess ( havenod ) ;
throw err ;
}
}
2021-12-16 20:10:40 -05:00
return havenod ;
2022-12-13 08:44:20 +00:00
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-05-01 13:30:11 -04:00
const srv = net . createServer ( ) ;
2022-02-09 01:41:00 -08:00
srv . listen ( 0 , function ( ) {
2022-05-01 13:30:11 -04:00
const port = ( srv . address ( ) as net . AddressInfo ) . port ;
2022-02-09 01:41:00 -08:00
srv . close ( function ( ) {
resolve ( port ) ;
} ) ;
} ) ;
} ) ;
}
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-27 12:24:55 -04:00
async function releaseHavenoProcess ( havenod : HavenoClient , deleteAppDir? : boolean ) {
2023-10-02 08:16:54 -04:00
moneroTs . GenUtils . remove ( HAVENO_PROCESSES , havenod ) ;
moneroTs . GenUtils . remove ( HAVENO_PROCESS_PORTS , getPort ( havenod . getUrl ( ) ) ) ;
2022-02-09 01:41:00 -08:00
try {
await havenod . shutdownServer ( ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2023-04-07 18:09:30 -04:00
assert ( err . message . indexOf ( OFFLINE_ERR_MSG ) >= 0 , "Unexpected error shutting down server: " + err . message ) ;
2022-02-09 01:41:00 -08:00
}
2022-04-27 12:24:55 -04:00
if ( deleteAppDir ) deleteHavenoInstance ( havenod ) ;
}
/ * *
* Delete a Haveno instance from disk .
* /
function deleteHavenoInstance ( havenod : HavenoClient ) {
if ( ! havenod . getAppName ( ) ) throw new Error ( "Cannot delete Haveno instance owned by different process" )
2023-01-23 16:17:00 -05:00
deleteHavenoInstanceByAppName ( havenod . getAppName ( ) ! ) ;
}
function deleteHavenoInstanceByAppName ( appName : string ) {
const userDataDir = process . env . APPDATA || ( process . platform === 'darwin' ? process . env . HOME + '/Library/Application Support' : process . env . HOME + "/.local/share" ) ;
const appPath = path . normalize ( userDataDir + "/" + appName ) ;
fs . rmSync ( appPath , { recursive : true , force : true } ) ;
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() {
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// init client connected to monero-wallet-rpc
2023-10-02 08:16:54 -04:00
fundingWallet = await moneroTs . connectToWalletRpc ( TestConfig . fundingWallet . url , TestConfig . fundingWallet . username , TestConfig . fundingWallet . password ) ;
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// check if wallet is open
let walletIsOpen = false
try {
await fundingWallet . getPrimaryAddress ( ) ;
walletIsOpen = true ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
// do nothing
}
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// open wallet if necessary
if ( ! walletIsOpen ) {
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// attempt to open funding wallet
try {
2022-07-30 17:11:40 -04:00
await fundingWallet . openWallet ( { path : TestConfig.fundingWallet.defaultPath , password : TestConfig.fundingWallet.walletPassword } ) ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2023-10-02 08:16:54 -04:00
if ( ! ( err instanceof moneroTs . MoneroRpcError ) ) throw err ;
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// -1 returned when wallet does not exist or fails to open e.g. it's already open by another application
2022-05-01 13:30:11 -04:00
if ( err . getCode ( ) === - 1 ) {
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// create wallet
2022-08-06 18:38:31 -04:00
await fundingWallet . createWallet ( {
path : TestConfig.fundingWallet.defaultPath ,
password : TestConfig.fundingWallet.walletPassword ,
2023-07-25 09:14:04 -04:00
seed : TestConfig.fundingWallet.seed ,
2022-08-06 18:38:31 -04:00
restoreHeight : TestConfig.fundingWallet.restoreHeight
} ) ;
2021-11-12 10:26:22 -05:00
} else {
2022-05-01 13:30:11 -04:00
throw err ;
2021-11-12 10:26:22 -05:00
}
}
}
}
2022-08-12 08:28:58 -04:00
async function prepareForTrading ( numTrades : number , . . . havenods : HavenoClient [ ] ) {
2022-12-13 08:44:20 +00:00
2023-10-10 07:02:36 -04:00
// create payment account for each payment method
for ( const havenod of havenods ) {
for ( const paymentMethod of await havenod . getPaymentMethods ( ) ) {
if ( await hasPaymentAccount ( { trader : havenod , paymentMethod : paymentMethod.getId ( ) } ) ) continue ; // skip if exists
const accountForm = await user1 . getPaymentAccountForm ( paymentMethod . getId ( ) ) ;
for ( const field of accountForm . getFieldsList ( ) ) field . setValue ( getValidFormInput ( accountForm , field . getId ( ) ) ) ; // set all form fields
await havenod . createPaymentAccount ( accountForm ) ;
}
}
// create payment account for each asset code
2022-08-12 08:28:58 -04:00
for ( const havenod of havenods ) {
2023-10-28 13:35:12 -04:00
for ( const assetCode of TestConfig . assetCodes . concat ( TestConfig . fixedPriceAssetCodes ) ) {
2023-10-10 07:02:36 -04:00
if ( await hasPaymentAccount ( { trader : havenod , assetCode : assetCode } ) ) continue ; // skip if exists
await createPaymentAccount ( havenod , assetCode ) ;
2022-08-12 08:28:58 -04:00
}
}
2022-12-13 08:44:20 +00:00
2022-08-12 08:28:58 -04:00
// fund wallets
2023-12-27 16:58:30 -05:00
const tradeAmount = 500000000000 n ;
2023-10-04 08:44:55 -04:00
const wallets : moneroTs.MoneroWallet [ ] = [ ] ;
2022-08-12 08:28:58 -04:00
for ( const havenod of havenods ) wallets . push ( await getWallet ( havenod ) ) ;
2023-12-27 16:58:30 -05:00
await fundOutputs ( wallets , tradeAmount * 2 n , numTrades ) ;
2022-08-12 08:28:58 -04:00
}
async function getWallet ( havenod : HavenoClient ) {
2022-11-04 15:57:42 -04:00
if ( ! HAVENO_WALLETS . has ( havenod ) ) {
let wallet : any ;
if ( havenod === user1 ) wallet = user1Wallet ;
else if ( havenod === user2 ) wallet = user2Wallet ;
2023-10-02 08:16:54 -04:00
else wallet = await moneroTs . connectToWalletRpc ( "http://127.0.0.1:" + havenod . getWalletRpcPort ( ) , TestConfig . defaultHavenod . walletUsername , TestConfig . defaultHavenod . accountPassword ) ;
2022-11-04 15:57:42 -04:00
HAVENO_WALLETS . set ( havenod , wallet ) ;
}
return HAVENO_WALLETS . get ( havenod ) ;
2022-08-12 08:28:58 -04:00
}
2022-10-01 07:47:46 -04:00
async function startMining ( ) : Promise < boolean > {
2022-04-06 11:28:56 -04:00
try {
2022-10-01 07:47:46 -04:00
const numThreads = getBaseCurrencyNetwork ( ) === BaseCurrencyNetwork . XMR_LOCAL ? 1 : Math.max ( 1 , Math . floor ( os . cpus ( ) . length * TestConfig . maxCpuPct ) ) ;
await monerod . startMining ( await fundingWallet . getPrimaryAddress ( ) , numThreads ) ;
2024-01-05 06:29:32 -05:00
HavenoUtils . log ( 2 , "Mining started" ) ;
2022-10-01 07:47:46 -04:00
return true ;
2022-05-01 13:30:11 -04:00
} catch ( err : any ) {
2022-04-06 11:28:56 -04:00
if ( err . message !== "Already mining" ) throw err ;
2022-10-01 07:47:46 -04:00
HavenoUtils . log ( 2 , ( "Already mining" ) ) ;
return false ;
2022-04-06 11:28:56 -04:00
}
}
2022-10-01 07:47:46 -04:00
async function stopMining() {
await monerod . stopMining ( ) ;
HavenoUtils . log ( 2 , "Mining stopped" ) ;
}
2022-11-04 15:57:42 -04:00
async function mineBlocks ( numBlocks : number ) {
await mineToHeight ( await monerod . getHeight ( ) + numBlocks ) ;
}
async function mineToHeight ( height : number ) {
if ( await monerod . getHeight ( ) >= height ) return ;
const miningStarted = await startMining ( ) ;
while ( await monerod . getHeight ( ) < height ) {
2023-10-02 08:16:54 -04:00
await moneroTs . GenUtils . waitFor ( TestConfig . trade . walletSyncPeriodMs ) ;
2022-11-04 15:57:42 -04:00
}
if ( miningStarted ) await stopMining ( ) ;
}
2024-01-13 08:39:27 -05:00
async function mineToUnlock ( txHash : string ) {
let tx = await monerod . getTx ( txHash ) ;
if ( tx && tx . getNumConfirmations ( ) >= 10 ) return ; // TODO: tx.getIsLocked()
const miningStarted = await startMining ( ) ;
while ( ! tx || tx . getNumConfirmations ( ) < 10 ) {
await moneroTs . GenUtils . waitFor ( TestConfig . trade . walletSyncPeriodMs ) ;
tx = await monerod . getTx ( txHash ) ;
}
if ( miningStarted ) await stopMining ( ) ;
}
2021-11-12 10:26:22 -05:00
/ * *
* Wait for unlocked balance in wallet or Haveno daemon .
* /
2022-08-17 12:22:08 -04:00
async function waitForAvailableBalance ( amount : bigint , . . . wallets : any [ ] ) {
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// wrap common wallet functionality for tests
class WalletWrapper {
2022-12-13 08:44:20 +00:00
2023-10-04 08:44:55 -04:00
_wallet : moneroTs.MoneroWallet ;
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
constructor ( wallet : any ) {
this . _wallet = wallet ;
}
2022-12-13 08:44:20 +00:00
2022-08-17 12:22:08 -04:00
async getAvailableBalance ( ) : Promise < bigint > {
if ( this . _wallet instanceof HavenoClient ) return BigInt ( ( await this . _wallet . getBalances ( ) ) . getAvailableBalance ( ) ) ;
2023-10-04 08:44:55 -04:00
else return await this . _wallet . getUnlockedBalance ( ) ;
2021-11-12 10:26:22 -05:00
}
2022-12-13 08:44:20 +00:00
2022-08-17 12:22:08 -04:00
async getPendingBalance ( ) : Promise < bigint > {
if ( this . _wallet instanceof HavenoClient ) return BigInt ( ( await this . _wallet . getBalances ( ) ) . getPendingBalance ( ) ) ;
2023-10-04 08:44:55 -04:00
else return await this . _wallet . getBalance ( ) - await this . getAvailableBalance ( ) ;
2021-11-12 10:26:22 -05:00
}
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
async getDepositAddress ( ) : Promise < string > {
2022-05-15 13:46:56 -04:00
if ( this . _wallet instanceof HavenoClient ) return await this . _wallet . getXmrNewSubaddress ( ) ;
2023-10-04 08:44:55 -04:00
else return ( await this . _wallet . createSubaddress ( 0 ) ) . getAddress ( ) ;
2021-11-12 10:26:22 -05:00
}
}
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// wrap wallets
for ( let i = 0 ; i < wallets . length ; i ++ ) wallets [ i ] = new WalletWrapper ( wallets [ i ] ) ;
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// fund wallets with insufficient balance
let miningNeeded = false ;
2023-10-02 08:16:54 -04:00
const fundConfig : moneroTs.MoneroTxConfig = new moneroTs . MoneroTxConfig ( { accountIndex : 0 , relay : true } ) ;
2022-05-01 13:30:11 -04:00
for ( const wallet of wallets ) {
2022-08-17 12:22:08 -04:00
const availableBalance = await wallet . getAvailableBalance ( ) ;
if ( availableBalance < amount ) miningNeeded = true ;
const depositNeeded : bigint = amount - availableBalance - await wallet . getPendingBalance ( ) ;
2023-12-27 16:58:30 -05:00
if ( depositNeeded > 0 n && wallet . _wallet !== fundingWallet ) {
2022-01-15 15:43:10 -05:00
for ( let i = 0 ; i < 5 ; i ++ ) {
2023-12-27 16:58:30 -05:00
fundConfig . addDestination ( await wallet . getDepositAddress ( ) , depositNeeded * 2 n ) ; // make several deposits
2022-01-15 15:43:10 -05:00
}
}
2021-11-12 10:26:22 -05:00
}
if ( fundConfig . getDestinations ( ) ) {
2022-08-17 12:22:08 -04:00
await waitForAvailableBalance ( 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 ) ; }
2022-05-01 13:30:11 -04:00
catch ( err : any ) { throw new Error ( "Error funding wallets: " + err . message ) ; }
2021-11-12 10:26:22 -05:00
}
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// done if all wallets have sufficient unlocked balance
if ( ! miningNeeded ) return ;
2022-12-13 08:44:20 +00:00
2021-11-12 10:26:22 -05:00
// wait for funds to unlock
2022-10-01 07:47:46 -04:00
const miningStarted = await startMining ( ) ;
2022-07-07 09:11:50 -04:00
HavenoUtils . log ( 1 , "Mining for unlocked balance of " + amount ) ;
2022-05-01 13:30:11 -04:00
const promises : Promise < void > [ ] = [ ] ;
for ( const wallet of wallets ) {
2022-07-07 09:11:50 -04:00
if ( wallet . _wallet === fundingWallet ) {
const subaddress = await fundingWallet . createSubaddress ( 0 ) ;
HavenoUtils . log ( 0 , "Mining to funding wallet. Alternatively, deposit to: " + subaddress . getAddress ( ) ) ;
}
2022-05-01 13:30:11 -04:00
// eslint-disable-next-line no-async-promise-executor
promises . push ( new Promise ( async ( resolve ) = > {
2023-10-02 08:16:54 -04:00
const taskLooper : any = new moneroTs . TaskLooper ( async function ( ) {
2022-08-18 10:01:05 -04:00
if ( await wallet . getAvailableBalance ( ) >= amount ) {
2021-11-12 10:26:22 -05:00
taskLooper . stop ( ) ;
resolve ( ) ;
}
} ) ;
taskLooper . start ( 5000 ) ;
} ) ) ;
}
await Promise . all ( promises ) ;
2022-10-01 07:47:46 -04:00
if ( miningStarted ) await stopMining ( ) ;
2022-04-06 11:28:56 -04:00
HavenoUtils . log ( 0 , "Funds unlocked, done mining" ) ;
2022-05-01 13:30:11 -04:00
}
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" ) ;
2022-10-01 07:47:46 -04:00
const miningStarted = await startMining ( ) ;
2022-05-01 13:30:11 -04:00
const promises : Promise < void > [ ] = [ ] ;
for ( const txHash of txHashes ) {
// eslint-disable-next-line no-async-promise-executor
promises . push ( new Promise ( async ( resolve ) = > {
2023-10-02 08:16:54 -04:00
const taskLooper = new moneroTs . TaskLooper ( async function ( ) {
2022-05-01 13:30:11 -04:00
const tx = await monerod . getTx ( txHash ) ;
2022-11-04 15:57:42 -04:00
if ( ! tx ) HavenoUtils . log ( 1 , "WARNING: tx hash " + txHash + " not found" ) ;
2023-10-02 08:16:54 -04:00
else if ( tx . getIsConfirmed ( ) && tx . getBlock ( ) . getHeight ( ) <= await monerod . getHeight ( ) - 10 ) {
2021-11-12 10:26:22 -05:00
taskLooper . stop ( ) ;
resolve ( ) ;
}
} ) ;
taskLooper . start ( 5000 ) ;
} ) ) ;
}
await Promise . all ( promises ) ;
2022-10-01 07:47:46 -04:00
HavenoUtils . log ( 1 , "Done waiting for txs to unlock" ) ;
if ( miningStarted ) await stopMining ( ) ;
2021-11-12 10:26:22 -05:00
}
2022-04-06 11:28:56 -04:00
/ * *
2022-05-31 14:05:31 -04:00
* Indicates if the given wallets have unspent outputs .
2022-12-13 08:44:20 +00:00
*
2022-05-14 19:35:02 -04:00
* @param { MoneroWallet [ ] } wallets - wallets to check
2022-04-06 11:28:56 -04:00
* @param { BigInt } amt - amount to check
* @param { number ? } numOutputs - number of outputs of the given amount ( default 1 )
2022-05-14 19:35:02 -04:00
* @param { boolean ? } isLocked - specifies if the outputs must be locked or unlocked ( default either )
2022-04-06 11:28:56 -04:00
* /
2022-05-14 19:35:02 -04:00
async function hasUnspentOutputs ( wallets : any [ ] , amt : BigInt , numOutputs? : number , isLocked? : boolean ) : Promise < boolean > {
2022-04-06 11:28:56 -04:00
if ( numOutputs === undefined ) numOutputs = 1 ;
2022-05-14 19:35:02 -04:00
for ( const wallet of wallets ) {
2023-10-04 08:44:55 -04:00
const unspentOutputs = await wallet . getOutputs ( { isSpent : false , isFrozen : false , minAmount : amt , txQuery : { isLocked : isLocked } } ) ;
2022-05-14 19:35:02 -04:00
if ( unspentOutputs . length < numOutputs ) return false ;
}
return true ;
2022-04-06 11:28:56 -04:00
}
/ * *
* Fund the given wallets .
2022-12-13 08:44:20 +00:00
*
2022-04-06 11:28:56 -04:00
* @param { MoneroWallet } wallets - monerojs wallets
* @param { BigInt } amt - the amount to fund
2023-10-02 08:16:54 -04:00
* @param { number } [ numOutputs ] - the number of outputs of the given amount ( default 1 )
* @param { boolean } [ waitForUnlock ] - wait for outputs to unlock ( default false )
2022-04-06 11:28:56 -04:00
* /
2023-10-04 08:44:55 -04:00
async function fundOutputs ( wallets : moneroTs.MoneroWallet [ ] , amt : bigint , numOutputs? : number , waitForUnlock? : boolean ) : Promise < void > {
2022-04-06 11:28:56 -04:00
if ( numOutputs === undefined ) numOutputs = 1 ;
2022-05-14 19:35:02 -04:00
if ( waitForUnlock === undefined ) waitForUnlock = true ;
2022-12-13 08:44:20 +00:00
2022-04-06 11:28:56 -04:00
// collect destinations
2023-10-02 08:16:54 -04:00
const destinations : moneroTs.MoneroDestination [ ] = [ ] ;
2022-05-01 13:30:11 -04:00
for ( const wallet of wallets ) {
2022-07-07 09:11:50 -04:00
if ( await hasUnspentOutputs ( [ wallet ] , amt , numOutputs , undefined ) ) continue ;
2022-04-06 11:28:56 -04:00
for ( let i = 0 ; i < numOutputs ; i ++ ) {
2023-10-04 08:44:55 -04:00
destinations . push ( new moneroTs . MoneroDestination ( ( await wallet . createSubaddress ( 0 ) ) . getAddress ( ) , amt ) ) ;
2022-04-06 11:28:56 -04:00
}
}
2022-05-31 14:05:31 -04:00
if ( ! destinations . length ) return ;
2022-12-13 08:44:20 +00:00
2022-04-06 11:28:56 -04:00
// fund destinations
2023-10-02 08:16:54 -04:00
let txConfig = new moneroTs . MoneroTxConfig ( ) . setAccountIndex ( 0 ) . setRelay ( true ) ;
2022-05-01 13:30:11 -04:00
const txHashes : string [ ] = [ ] ;
2023-10-02 08:16:54 -04:00
let sendAmt = 0 n ;
2022-04-06 11:28:56 -04:00
for ( let i = 0 ; i < destinations . length ; i ++ ) {
2023-10-02 08:16:54 -04:00
txConfig . addDestination ( destinations [ i ] , undefined ) ; // TODO: remove once converted to MoneroTxConfig.ts
sendAmt = sendAmt + destinations [ i ] . getAmount ( ) ;
2022-04-06 11:28:56 -04:00
if ( i === destinations . length - 1 || ( i > 0 && i % 15 === 0 ) ) {
2023-10-04 08:44:55 -04:00
await waitForAvailableBalance ( sendAmt , fundingWallet ) ;
2022-10-01 07:47:46 -04:00
const txs = await fundingWallet . createTxs ( txConfig ) ;
for ( const tx of txs ) txHashes . push ( tx . getHash ( ) ) ;
2023-10-02 08:16:54 -04:00
txConfig = new moneroTs . MoneroTxConfig ( ) . setAccountIndex ( 0 ) . setRelay ( true ) ;
sendAmt = 0 n ;
2022-04-06 11:28:56 -04:00
}
}
2022-11-04 15:57:42 -04:00
// if not waiting to unlock, wait to observe txs and return
if ( txHashes . length && ! waitForUnlock ) {
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs ) ;
2022-11-04 15:57:42 -04:00
return ;
}
// mine until outputs unlocked
2022-05-14 19:35:02 -04:00
let miningStarted = false ;
2022-11-04 15:57:42 -04:00
let miningAttempted = false ;
while ( ! await hasUnspentOutputs ( wallets , amt , numOutputs , false ) ) {
if ( waitForUnlock && ! miningAttempted ) {
2022-07-07 09:11:50 -04:00
HavenoUtils . log ( 1 , "Mining to fund outputs" ) ;
2022-10-01 07:47:46 -04:00
miningStarted = await startMining ( ) ;
2022-11-04 15:57:42 -04:00
miningAttempted = true ;
2022-05-14 19:35:02 -04:00
}
2022-12-21 15:25:52 +00:00
await wait ( TestConfig . trade . walletSyncPeriodMs ) ;
2021-11-12 10:26:22 -05:00
}
2022-10-01 07:47:46 -04:00
if ( miningStarted ) await stopMining ( ) ;
2021-11-12 10:26:22 -05:00
}
2022-10-01 07:47:46 -04:00
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function getBalancesStr ( balances : XmrBalanceInfo ) : string {
2023-10-04 08:44:55 -04:00
return "Balance: " + balances . getBalance ( ) + ",\n" +
"Available balance: " + balances . getAvailableBalance ( ) + ",\n" +
"Pending balance: " + balances . getPendingBalance ( ) + ",\n" +
"Reserved in offers: " + balances . getReservedOfferBalance ( ) + ",\n" +
"Locked in trade: " + balances . getReservedTradeBalance ( ) ;
2022-10-01 07:47:46 -04:00
}
2021-11-12 10:26:22 -05:00
async function wait ( durationMs : number ) {
return new Promise ( function ( resolve ) { setTimeout ( resolve , durationMs ) ; } ) ;
}
2022-10-01 07:47:46 -04:00
function getNotifications ( notifications : NotificationMessage [ ] , notificationType : NotificationMessage.NotificationType , tradeId? : string ) {
2022-05-01 13:30:11 -04:00
const filteredNotifications : NotificationMessage [ ] = [ ] ;
2022-10-01 07:47:46 -04:00
for ( const notification of notifications ) {
if ( notification . getType ( ) !== notificationType ) continue ;
if ( tradeId ) {
let found = false ;
2022-10-14 16:09:10 -04:00
if ( notification . getTrade ( ) && notification . getTrade ( ) ! . getTradeId ( ) === tradeId ) found = true ;
if ( notification . getChatMessage ( ) && notification . getChatMessage ( ) ! . getTradeId ( ) === tradeId ) found = true ;
2022-10-01 07:47:46 -04:00
if ( ! found ) continue ;
}
filteredNotifications . push ( notification ) ;
2022-01-15 15:43:10 -05:00
}
return filteredNotifications ;
}
2022-10-14 16:09:10 -04:00
function getConnection ( connections : UrlConnection [ ] , url : string ) : UrlConnection | undefined {
2022-05-01 13:30:11 -04:00
for ( const connection of connections ) if ( connection . getUrl ( ) === url ) return connection ;
2022-02-09 01:41:00 -08:00
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
2022-05-01 13:30:11 -04:00
for ( const incomingTransfer of tx . getIncomingTransfersList ( ) ) testTransfer ( incomingTransfer , ctx ) ;
2021-12-30 22:03:00 +02:00
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 ) {
2023-12-27 16:58:30 -05:00
expect ( BigInt ( transfer . getAmount ( ) ) ) . toBeGreaterThanOrEqual ( 0 n ) ;
2021-12-30 22:03:00 +02:00
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 ) ;
2022-05-01 13:30:11 -04:00
for ( const subaddressIdx of transfer . getSubaddressIndicesList ( ) ) assert ( subaddressIdx >= 0 ) ;
2022-12-13 08:44:20 +00:00
2021-12-30 22:03:00 +02:00
// test destinations sum to outgoing amount
if ( transfer . getDestinationsList ( ) . length > 0 ) {
2023-12-27 16:58:30 -05:00
let sum = 0 n ;
2022-05-01 13:30:11 -04:00
for ( const destination of transfer . getDestinationsList ( ) ) {
2021-12-30 22:03:00 +02:00
testDestination ( destination ) ;
2023-12-27 16:58:30 -05:00
expect ( BigInt ( destination . getAmount ( ) ) ) . toBeGreaterThan ( 0 n ) ;
2021-12-30 22:03:00 +02:00
sum += BigInt ( destination . getAmount ( ) ) ;
}
assert . equal ( sum , BigInt ( transfer . getAmount ( ) ) ) ;
}
}
function testDestination ( destination : XmrDestination ) {
assert ( destination . getAddress ( ) ) ;
2023-12-27 16:58:30 -05:00
expect ( BigInt ( destination . getAmount ( ) ) ) . toBeGreaterThan ( 0 n ) ;
2021-12-30 22:03:00 +02:00
}
2024-04-07 08:13:09 -04:00
function getRandomBigIntWithinPercent ( base : bigint , percent : number ) : bigint {
return getRandomBigIntWithinRange ( base - multiply ( base , percent ) , base + multiply ( base , percent ) ) ;
}
function multiply ( amount : bigint , multiplier : number ) : bigint {
return BigInt ( Math . round ( Number ( amount ) * multiplier ) ) ;
}
function getRandomBigIntWithinRange ( min : bigint , max : bigint ) : bigint {
return BigInt ( Math . floor ( Math . random ( ) * ( Number ( max ) - Number ( min ) ) ) + Number ( min ) ) ;
}
2022-02-16 10:09:59 -05:00
function getRandomAssetCode() {
2023-10-02 08:16:54 -04:00
return TestConfig . assetCodes [ moneroTs . GenUtils . getRandomInt ( 0 , TestConfig . assetCodes . length - 1 ) ] ;
2022-05-26 10:58:15 -04:00
}
2023-10-10 07:02:36 -04:00
async function hasPaymentAccount ( config : { trader : HavenoClient ; assetCode? : string ; paymentMethod? : string } ) : Promise < boolean > {
for ( const paymentAccount of await config . trader . getPaymentAccounts ( ) ) {
if ( config . assetCode ? . toUpperCase ( ) === paymentAccount . getSelectedTradeCurrency ( ) ! . getCode ( ) ) return true ;
if ( config . paymentMethod ? . toUpperCase ( ) === paymentAccount . getPaymentMethod ( ) ! . getId ( ) ) return true ;
2022-05-26 10:58:15 -04:00
}
return false ;
2022-02-16 10:09:59 -05:00
}
function isCrypto ( assetCode : string ) {
2022-05-26 10:58:15 -04:00
return getCryptoAddress ( assetCode ) !== undefined ;
2022-02-16 10:09:59 -05:00
}
2022-10-14 16:09:10 -04:00
function getCryptoAddress ( currencyCode : string ) : string | undefined {
2022-05-26 10:58:15 -04:00
for ( const cryptoAddress of TestConfig . cryptoAddresses ) {
if ( cryptoAddress . currencyCode === currencyCode . toUpperCase ( ) ) return cryptoAddress . address ;
}
2022-02-16 10:09:59 -05:00
}
2023-08-29 10:32:50 -04:00
async function createPaymentAccount ( trader : HavenoClient , assetCodes : string , paymentMethodId? : string | PaymentAccountForm . FormId ) {
2023-10-28 13:35:12 -04:00
if ( ! paymentMethodId ) paymentMethodId = isCrypto ( assetCodes ! ) ? PaymentAccountForm.FormId.BLOCK_CHAINS : PaymentAccountForm.FormId.PAY_BY_MAIL ;
2023-08-31 09:15:04 -04:00
const accountForm = await trader . getPaymentAccountForm ( paymentMethodId ) ;
for ( const field of accountForm . getFieldsList ( ) ) field . setValue ( getValidFormInput ( accountForm , field . getId ( ) ) ) ;
2023-11-01 06:39:35 -04:00
if ( HavenoUtils . hasFormField ( accountForm , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) ) HavenoUtils . setFormValue ( accountForm , PaymentAccountFormField . FieldId . TRADE_CURRENCIES , assetCodes ) ;
2023-08-31 09:15:04 -04:00
return await trader . createPaymentAccount ( accountForm ) ;
2022-02-16 10:09:59 -05:00
}
2022-04-07 16:35:48 -04:00
async function createCryptoPaymentAccount ( trader : HavenoClient , currencyCode = "eth" ) : Promise < PaymentAccount > {
2022-05-01 13:30:11 -04:00
for ( const cryptoAddress of TestConfig . cryptoAddresses ) {
2022-02-16 10:09:59 -05:00
if ( cryptoAddress . currencyCode . toLowerCase ( ) !== currencyCode . toLowerCase ( ) ) continue ;
2022-02-11 17:13:56 -06:00
return trader . createCryptoPaymentAccount (
2023-10-02 08:16:54 -04:00
cryptoAddress . currencyCode + " " + cryptoAddress . address . substr ( 0 , 8 ) + "... " + moneroTs . GenUtils . getUUID ( ) ,
2022-02-16 10:09:59 -05:00
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-10-14 16:09:10 -04:00
function getOffer ( offers : OfferInfo [ ] , id : string ) : OfferInfo | undefined {
2021-09-14 08:30:22 -04:00
return offers . find ( offer = > offer . getId ( ) === id ) ;
}
2022-04-10 17:00:15 -04:00
function testCryptoPaymentAccount ( acct : PaymentAccount ) {
expect ( acct . getId ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( acct . getAccountName ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( acct . getPaymentAccountPayload ( ) ! . getCryptoCurrencyAccountPayload ( ) ! . getAddress ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( acct . getSelectedTradeCurrency ( ) ! . getCode ( ) . length ) . toBeGreaterThan ( 0 ) ;
expect ( acct . getTradeCurrenciesList ( ) . length ) . toEqual ( 1 ) ;
2022-05-01 13:30:11 -04:00
const tradeCurrency = acct . getTradeCurrenciesList ( ) [ 0 ] ;
2021-11-11 13:48:31 -05:00
expect ( tradeCurrency . getName ( ) . length ) . toBeGreaterThan ( 0 ) ;
2022-04-10 17:00:15 -04:00
expect ( tradeCurrency . getCode ( ) ) . toEqual ( acct . getSelectedTradeCurrency ( ) ! . getCode ( ) ) ;
}
function testCryptoPaymentAccountsEqual ( acct1 : PaymentAccount , acct2 : PaymentAccount ) {
expect ( acct1 . getId ( ) ) . toEqual ( acct2 . getId ( ) ) ;
expect ( acct1 . getAccountName ( ) ) . toEqual ( acct2 . getAccountName ( ) ) ;
expect ( acct1 . getSelectedTradeCurrency ( ) ! . getCode ( ) ) . toEqual ( acct2 . getSelectedTradeCurrency ( ) ! . getCode ( ) ) ;
expect ( acct1 . getPaymentAccountPayload ( ) ! . getCryptoCurrencyAccountPayload ( ) ! . getAddress ( ) ) . toEqual ( acct2 . getPaymentAccountPayload ( ) ! . getCryptoCurrencyAccountPayload ( ) ! . getAddress ( ) ) ;
2021-09-14 08:27:45 -04:00
}
2023-10-28 10:20:56 -04:00
function testOffer ( offer : OfferInfo , ctx? : Partial < TradeContext > ) {
2023-10-30 17:11:33 -04:00
expect ( offer . getId ( ) . length ) . toBeGreaterThan ( 0 ) ;
2023-03-06 13:27:52 -05:00
if ( ctx ) {
2023-10-30 17:11:33 -04:00
expect ( offer . getBuyerSecurityDepositPct ( ) ) . toEqual ( ctx ? . securityDepositPct ) ;
expect ( offer . getSellerSecurityDepositPct ( ) ) . toEqual ( ctx ? . securityDepositPct ) ;
2023-03-06 13:27:52 -05:00
expect ( offer . getUseMarketBasedPrice ( ) ) . toEqual ( ! ctx ? . price ) ;
expect ( offer . getMarketPriceMarginPct ( ) ) . toEqual ( ctx ? . priceMargin ? ctx?.priceMargin : 0 ) ;
2023-10-30 17:11:33 -04:00
2023-03-06 13:27:52 -05:00
// TODO: test rest of offer
2022-03-07 09:57:00 -08:00
}
2022-03-09 04:43:30 -08:00
}
2023-11-25 14:48:58 -05:00
function testMoneroNodeSettingsEqual ( settingsBefore : XmrNodeSettings , settingsAfter : XmrNodeSettings ) {
2024-01-17 11:22:12 -05:00
expect ( settingsAfter . getBlockchainPath ( ) ) . toEqual ( settingsBefore . getBlockchainPath ( ) ) ;
expect ( settingsAfter . getBootstrapUrl ( ) ) . toEqual ( settingsBefore . getBootstrapUrl ( ) ) ;
expect ( settingsAfter . getStartupFlagsList ( ) ) . toEqual ( settingsBefore . getStartupFlagsList ( ) ) ;
2022-06-16 21:51:22 -04:00
}
function getFormField ( form : PaymentAccountForm , fieldId : PaymentAccountFormField.FieldId ) : PaymentAccountFormField {
for ( const field of form . getFieldsList ( ) ) {
if ( field . getId ( ) == fieldId ) return field ;
}
throw new Error ( "Form field not found: " + fieldId ) ;
}
2023-08-26 06:55:56 -04:00
function getValidFormInput ( form : PaymentAccountForm , fieldId : PaymentAccountFormField.FieldId ) : string {
2022-06-16 21:51:22 -04:00
const field = getFormField ( form , fieldId ) ;
switch ( fieldId ) {
case PaymentAccountFormField . FieldId . ACCEPTED_COUNTRY_CODES :
2023-08-26 06:55:56 -04:00
if ( form . getId ( ) === PaymentAccountForm . FormId . SEPA || form . getId ( ) === PaymentAccountForm . FormId . SEPA_INSTANT ) return "BE," + field . getSupportedSepaEuroCountriesList ( ) . map ( country = > country . getCode ( ) ) . join ( ',' ) ;
2022-06-16 21:51:22 -04:00
return field . getSupportedCountriesList ( ) . map ( country = > country . getCode ( ) ) . join ( ',' ) ;
case PaymentAccountFormField . FieldId . ACCOUNT_ID :
2022-06-23 10:40:10 -04:00
return "jdoe@no.com" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . ACCOUNT_NAME :
2023-10-02 08:16:54 -04:00
return "Form_" + form . getId ( ) + " " + moneroTs . GenUtils . getUUID ( ) ; // TODO: rename to form.getPaymentMethodId()
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . ACCOUNT_NR :
2022-06-22 13:17:00 -04:00
return "12345678" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . ACCOUNT_OWNER :
2022-06-23 10:40:10 -04:00
return "John Doe" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . ACCOUNT_TYPE :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . ANSWER :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_ACCOUNT_NAME :
2024-06-04 11:55:38 -04:00
return "John Doe" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . BANK_ACCOUNT_NUMBER :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_ACCOUNT_TYPE :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_ADDRESS :
return "456 example st" ;
case PaymentAccountFormField . FieldId . BANK_BRANCH :
return "Bank branch XYZ" ;
case PaymentAccountFormField . FieldId . BANK_BRANCH_CODE :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_BRANCH_NAME :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_NAME :
return "Bank XYZ" ;
case PaymentAccountFormField . FieldId . BANK_SWIFT_CODE :
return "12345678901" ; // TODO: use real swift code
case PaymentAccountFormField . FieldId . BENEFICIARY_ACCOUNT_NR :
return "1234567890" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_ADDRESS :
return "123 example st" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_CITY :
return "Acme" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_NAME :
return "Jane Doe" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_PHONE :
return "123-456-7890" ;
case PaymentAccountFormField . FieldId . BIC :
return "ATLNFRPP" ;
case PaymentAccountFormField . FieldId . BRANCH_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . CITY :
return "Atlanta" ;
case PaymentAccountFormField . FieldId . CONTACT :
return "Email please" ;
case PaymentAccountFormField . FieldId . COUNTRY :
case PaymentAccountFormField . FieldId . BANK_COUNTRY_CODE :
case PaymentAccountFormField . FieldId . INTERMEDIARY_COUNTRY_CODE :
2022-12-13 08:44:20 +00:00
return field . getSupportedCountriesList ( ) . length ? field . getSupportedCountriesList ( ) [ 0 ] ! . getCode ( ) : "FR" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . EMAIL :
return "jdoe@no.com" ;
case PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR :
return "876-512-7813" ;
2024-06-08 10:50:50 -04:00
case PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR_OR_USERNAME :
return "john.doe"
case PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR_OR_CASHTAG :
return "john.doe"
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . EXTRA_INFO :
return "Please and thanks" ;
case PaymentAccountFormField . FieldId . HOLDER_ADDRESS :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . HOLDER_EMAIL :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . HOLDER_NAME :
2022-07-15 10:09:56 -04:00
return "user1 Doe" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . HOLDER_TAX_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . IBAN :
return "FR1420041010050500013M02606" ;
case PaymentAccountFormField . FieldId . IFSC :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_ADDRESS :
return "123 intermediary example st" ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_BRANCH :
return "Intermediary bank branch XYZ" ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_NAME :
return "Intermediary bank XYZ" ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_SWIFT_CODE :
return "10987654321" ; // TODO: use real swift code
case PaymentAccountFormField . FieldId . MOBILE_NR :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . NATIONAL_ACCOUNT_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . PAYID :
2024-06-04 11:55:38 -04:00
return "john.doe@example.com" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . PIX_KEY :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . POSTAL_ADDRESS :
2023-08-29 06:25:17 -04:00
return "123 street" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . PROMPT_PAY_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . QUESTION :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . REQUIREMENTS :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . SALT :
return "" ;
2022-06-22 13:17:00 -04:00
case PaymentAccountFormField . FieldId . SORT_CODE :
return "123456" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . SPECIAL_INSTRUCTIONS :
return "asap plz" ;
2022-11-23 09:41:48 +00:00
case PaymentAccountFormField . FieldId . STATE :
const country = HavenoUtils . getFormValue ( form , PaymentAccountFormField . FieldId . COUNTRY ) ;
2023-10-02 08:16:54 -04:00
return moneroTs . GenUtils . arrayContains ( field . getRequiredForCountriesList ( ) , country ) ? "My state" : "" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . TRADE_CURRENCIES :
2023-08-26 06:55:56 -04:00
if ( field . getComponent ( ) === PaymentAccountFormField . Component . SELECT_ONE ) {
if ( form . getId ( ) === PaymentAccountForm . FormId . F2F ) return "XAU" ;
2023-09-07 16:00:29 -04:00
if ( form . getId ( ) === PaymentAccountForm . FormId . PAY_BY_MAIL ) return "XGB" ;
2023-08-26 06:55:56 -04:00
return field . getSupportedCurrenciesList ( ) [ 0 ] ! . getCode ( ) ; // TODO: randomly select?
}
2022-11-23 09:41:48 +00:00
else return field . getSupportedCurrenciesList ( ) . map ( currency = > currency . getCode ( ) ) . join ( ',' ) ;
2024-06-08 10:50:50 -04:00
case PaymentAccountFormField . FieldId . USERNAME :
2022-06-16 21:51:22 -04:00
return "user123" ;
2022-11-23 09:41:48 +00:00
case PaymentAccountFormField . FieldId . ADDRESS :
const currencyCode = HavenoUtils . getFormValue ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) ;
for ( let cryptoAddress of TestConfig . cryptoAddresses ) {
if ( cryptoAddress . currencyCode . toLowerCase ( ) === currencyCode . toLowerCase ( ) ) return cryptoAddress . address ;
}
throw new Error ( "Unsupported blockchain currency code: " + currencyCode ) ;
2022-06-16 21:51:22 -04:00
default :
throw new Error ( "Unhandled form field: " + fieldId ) ;
}
}
// TODO: improve invalid inputs
function getInvalidFormInput ( form : PaymentAccountForm , fieldId : PaymentAccountFormField.FieldId ) : string {
2022-06-17 09:14:07 -04:00
const field = getFormField ( form , fieldId ) ;
2022-06-16 21:51:22 -04:00
switch ( fieldId ) {
case PaymentAccountFormField . FieldId . ACCEPTED_COUNTRY_CODES :
return "US,XX" ;
case PaymentAccountFormField . FieldId . ACCOUNT_ID :
2022-06-23 10:40:10 -04:00
return "" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . ACCOUNT_NAME :
return "" ;
case PaymentAccountFormField . FieldId . ACCOUNT_NR :
2022-06-22 13:17:00 -04:00
return "123457A" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . ACCOUNT_OWNER :
2022-06-23 10:40:10 -04:00
return "J" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . ACCOUNT_TYPE :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . ANSWER :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_ACCOUNT_NAME :
2024-06-04 11:55:38 -04:00
return "F" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . BANK_ACCOUNT_NUMBER :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_ACCOUNT_TYPE :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_ADDRESS :
return "" ;
case PaymentAccountFormField . FieldId . BANK_BRANCH :
return "A" ;
case PaymentAccountFormField . FieldId . BANK_BRANCH_CODE :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_BRANCH_NAME :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_CODE :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_COUNTRY_CODE :
return "A" ;
case PaymentAccountFormField . FieldId . BANK_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . BANK_NAME :
return "A" ;
case PaymentAccountFormField . FieldId . BANK_SWIFT_CODE :
return "A" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_ACCOUNT_NR :
return "1" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_ADDRESS :
return "" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_CITY :
return "A" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_NAME :
return "A" ;
case PaymentAccountFormField . FieldId . BENEFICIARY_PHONE :
return "1" ;
case PaymentAccountFormField . FieldId . BIC :
return "123" ;
case PaymentAccountFormField . FieldId . BRANCH_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . CITY :
return "A" ;
case PaymentAccountFormField . FieldId . CONTACT :
return "" ;
case PaymentAccountFormField . FieldId . COUNTRY :
return "abc"
case PaymentAccountFormField . FieldId . EMAIL :
return "@no.com" ;
case PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR :
return "" ; // TODO: validate phone numbers, e.g. 876
2024-06-08 10:50:50 -04:00
case PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR_OR_USERNAME :
return "A"
case PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR_OR_CASHTAG :
return "A"
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . EXTRA_INFO :
throw new Error ( "Extra info has no invalid input" ) ;
case PaymentAccountFormField . FieldId . HOLDER_ADDRESS :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . HOLDER_EMAIL :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . HOLDER_NAME :
return "A" ;
case PaymentAccountFormField . FieldId . HOLDER_TAX_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . IBAN :
return "abc" ;
case PaymentAccountFormField . FieldId . IFSC :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_ADDRESS :
return "" ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_BRANCH :
return "A" ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_COUNTRY_CODE :
return "A" ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_NAME :
return "A" ;
case PaymentAccountFormField . FieldId . INTERMEDIARY_SWIFT_CODE :
return "A" ;
case PaymentAccountFormField . FieldId . MOBILE_NR :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . NATIONAL_ACCOUNT_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . PAYID :
2024-06-04 11:55:38 -04:00
return "A" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . PIX_KEY :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . POSTAL_ADDRESS :
2023-08-29 06:25:17 -04:00
return "" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . PROMPT_PAY_ID :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . QUESTION :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . REQUIREMENTS :
throw new Error ( "Not implemented" ) ;
case PaymentAccountFormField . FieldId . SALT :
return "abc" ;
2022-06-22 13:17:00 -04:00
case PaymentAccountFormField . FieldId . SORT_CODE :
return "12345A" ;
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . SPECIAL_INSTRUCTIONS :
throw new Error ( "Special instructions have no invalid input" ) ;
2022-06-17 09:14:07 -04:00
case PaymentAccountFormField . FieldId . STATE : {
2022-11-23 09:41:48 +00:00
const country = HavenoUtils . getFormValue ( form , PaymentAccountFormField . FieldId . COUNTRY ) ;
2023-10-02 08:16:54 -04:00
return moneroTs . GenUtils . arrayContains ( field . getRequiredForCountriesList ( ) , country ) ? "" : "My state" ;
2022-06-17 09:14:07 -04:00
}
2022-06-16 21:51:22 -04:00
case PaymentAccountFormField . FieldId . TRADE_CURRENCIES :
return "abc,def" ;
2024-06-08 10:50:50 -04:00
case PaymentAccountFormField . FieldId . USERNAME :
2022-06-16 21:51:22 -04:00
return "A" ;
2022-11-23 09:41:48 +00:00
case PaymentAccountFormField . FieldId . ADDRESS :
return "A123" ;
2022-06-16 21:51:22 -04:00
default :
throw new Error ( "Unhandled form field: " + fieldId ) ;
}
}
2022-11-23 09:41:48 +00:00
function testPaymentAccount ( account : PaymentAccount , form : PaymentAccountForm ) {
if ( account . getPaymentAccountPayload ( ) ? . getCryptoCurrencyAccountPayload ( ) ) testCryptoPaymentAccount ( account ) ; // TODO: test non-crypto
2024-06-08 10:50:50 -04:00
expect ( account . getAccountName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . ACCOUNT_NAME ) . getValue ( ) ) ; // TODO: using number as payment method, account payload's account name = username
2022-10-14 16:09:10 -04:00
const isCountryBased = account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) !== undefined ;
if ( isCountryBased ) expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getCountryCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . COUNTRY ) . getValue ( ) ) ;
2022-06-16 21:51:22 -04:00
switch ( form . getId ( ) ) {
2022-11-23 09:41:48 +00:00
case PaymentAccountForm . FormId . BLOCK_CHAINS :
expect ( account . getPaymentAccountPayload ( ) ! . getCryptoCurrencyAccountPayload ( ) ! . getAddress ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . ADDRESS ) . getValue ( ) ) ;
expect ( account . getTradeCurrenciesList ( ) . map ( currency = > currency . getCode ( ) ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
case PaymentAccountForm . FormId . REVOLUT :
2024-06-08 10:50:50 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getRevolutAccountPayload ( ) ! . getUsername ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . USERNAME ) . getValue ( ) ) ;
2022-06-16 21:51:22 -04:00
expect ( account . getTradeCurrenciesList ( ) . map ( currency = > currency . getCode ( ) ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
case PaymentAccountForm . FormId . SEPA :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getSepaAccountPayload ( ) ! . getHolderName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . HOLDER_NAME ) . getValue ( ) ) ;
//expect(account.getPaymentAccountPayload()!.getCountryBasedPaymentAccountPayload()!.getSepaAccountPayload().getEmail()).toEqual(getFormField(form, PaymentAccountFormField.FieldId.EMAIL).getValue()); // TODO: if this is deprecated, remove from sepa model
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getSepaAccountPayload ( ) ! . getIban ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . IBAN ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getSepaAccountPayload ( ) ! . getBic ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BIC ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getAcceptedCountryCodesList ( ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . ACCEPTED_COUNTRY_CODES ) . getValue ( ) ) ;
2022-06-22 11:10:54 -04:00
break ;
case PaymentAccountForm . FormId . SEPA_INSTANT :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getSepaInstantAccountPayload ( ) ! . getHolderName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . HOLDER_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getSepaInstantAccountPayload ( ) ! . getIban ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . IBAN ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getSepaInstantAccountPayload ( ) ! . getBic ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BIC ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getAcceptedCountryCodesList ( ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . ACCEPTED_COUNTRY_CODES ) . getValue ( ) ) ;
2022-06-16 21:51:22 -04:00
break ;
case PaymentAccountForm . FormId . TRANSFERWISE :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getTransferwiseAccountPayload ( ) ! . getEmail ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EMAIL ) . getValue ( ) ) ;
2022-06-16 21:51:22 -04:00
break ;
2023-04-17 10:13:56 -04:00
case PaymentAccountForm . FormId . ZELLE :
expect ( account . getPaymentAccountPayload ( ) ! . getZelleAccountPayload ( ) ! . getHolderName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . HOLDER_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getZelleAccountPayload ( ) ! . getEmailOrMobileNr ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR ) . getValue ( ) ) ;
2024-06-08 10:50:50 -04:00
expect ( account . getTradeCurrenciesList ( ) . length ) . toEqual ( 1 ) ;
expect ( account . getTradeCurrenciesList ( ) [ 0 ] . getCode ( ) ) . toEqual ( "USD" ) ;
2022-06-16 21:51:22 -04:00
break ;
case PaymentAccountForm . FormId . SWIFT :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBankSwiftCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BANK_SWIFT_CODE ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBankCountryCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BANK_COUNTRY_CODE ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBankName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BANK_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBankBranch ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BANK_BRANCH ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBankAddress ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BANK_ADDRESS ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getIntermediarySwiftCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . INTERMEDIARY_SWIFT_CODE ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getIntermediaryCountryCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . INTERMEDIARY_COUNTRY_CODE ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getIntermediaryName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . INTERMEDIARY_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getIntermediaryBranch ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . INTERMEDIARY_BRANCH ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getIntermediaryAddress ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . INTERMEDIARY_ADDRESS ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBeneficiaryName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BENEFICIARY_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBeneficiaryAccountNr ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BENEFICIARY_ACCOUNT_NR ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBeneficiaryAddress ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BENEFICIARY_ADDRESS ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBeneficiaryCity ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BENEFICIARY_CITY ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getBeneficiaryPhone ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BENEFICIARY_PHONE ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getSwiftAccountPayload ( ) ! . getSpecialInstructions ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . SPECIAL_INSTRUCTIONS ) . getValue ( ) ) ;
2022-06-16 21:51:22 -04:00
break ;
case PaymentAccountForm . FormId . F2F :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getF2fAccountPayload ( ) ! . getCity ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . CITY ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getF2fAccountPayload ( ) ! . getContact ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . CONTACT ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getF2fAccountPayload ( ) ! . getExtraInfo ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EXTRA_INFO ) . getValue ( ) ) ;
2022-06-16 21:51:22 -04:00
break ;
case PaymentAccountForm . FormId . STRIKE :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getCountryBasedPaymentAccountPayload ( ) ! . getStrikeAccountPayload ( ) ! . getHolderName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . HOLDER_NAME ) . getValue ( ) ) ;
2022-06-16 21:51:22 -04:00
break ;
2022-06-17 09:14:07 -04:00
case PaymentAccountForm . FormId . MONEY_GRAM :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getMoneyGramAccountPayload ( ) ! . getCountryCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . COUNTRY ) . getValue ( ) ) ; // TODO: ok to not be CountryBasedPaymentAccountPayload?
expect ( account . getPaymentAccountPayload ( ) ! . getMoneyGramAccountPayload ( ) ! . getState ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . STATE ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getMoneyGramAccountPayload ( ) ! . getHolderName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . HOLDER_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getMoneyGramAccountPayload ( ) ! . getEmail ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EMAIL ) . getValue ( ) ) ;
2022-06-17 09:14:07 -04:00
expect ( account . getTradeCurrenciesList ( ) . map ( currency = > currency . getCode ( ) ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
2022-06-22 13:17:00 -04:00
case PaymentAccountForm . FormId . FASTER_PAYMENTS :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getFasterPaymentsAccountPayload ( ) ! . getHolderName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . HOLDER_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getFasterPaymentsAccountPayload ( ) ! . getSortCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . SORT_CODE ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getFasterPaymentsAccountPayload ( ) ! . getAccountNr ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . ACCOUNT_NR ) . getValue ( ) ) ;
2022-06-22 13:17:00 -04:00
break ;
2022-06-23 10:40:10 -04:00
case PaymentAccountForm . FormId . UPHOLD :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getUpholdAccountPayload ( ) ! . getAccountOwner ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . ACCOUNT_OWNER ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getUpholdAccountPayload ( ) ! . getAccountId ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . ACCOUNT_ID ) . getValue ( ) ) ;
2022-06-23 10:40:10 -04:00
break ;
2022-06-23 11:16:39 -04:00
case PaymentAccountForm . FormId . PAXUM :
2022-10-14 16:09:10 -04:00
expect ( account . getPaymentAccountPayload ( ) ! . getPaxumAccountPayload ( ) ! . getEmail ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EMAIL ) . getValue ( ) ) ;
2022-06-23 11:16:39 -04:00
expect ( account . getTradeCurrenciesList ( ) . map ( currency = > currency . getCode ( ) ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
2023-08-29 06:25:17 -04:00
case PaymentAccountForm . FormId . PAY_BY_MAIL :
expect ( account . getPaymentAccountPayload ( ) ! . getPayByMailAccountPayload ( ) ! . getContact ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . CONTACT ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getPayByMailAccountPayload ( ) ! . getPostalAddress ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . POSTAL_ADDRESS ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getPayByMailAccountPayload ( ) ! . getExtraInfo ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EXTRA_INFO ) . getValue ( ) ) ;
expect ( account . getTradeCurrenciesList ( ) . length ) . toEqual ( 1 ) ;
expect ( account . getTradeCurrenciesList ( ) [ 0 ] . getCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
2023-09-09 09:18:53 -04:00
case PaymentAccountForm . FormId . CASH_AT_ATM :
expect ( account . getPaymentAccountPayload ( ) ! . getCashAtAtmAccountPayload ( ) ! . getExtraInfo ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EXTRA_INFO ) . getValue ( ) ) ;
expect ( account . getTradeCurrenciesList ( ) . length ) . toEqual ( 1 ) ;
expect ( account . getTradeCurrenciesList ( ) [ 0 ] . getCode ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
2024-06-04 11:55:38 -04:00
case PaymentAccountForm . FormId . AUSTRALIA_PAYID :
expect ( account . getPaymentAccountPayload ( ) ! . getAustraliaPayidPayload ( ) ! . getBankAccountName ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . BANK_ACCOUNT_NAME ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getAustraliaPayidPayload ( ) ! . getPayid ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . PAYID ) . getValue ( ) ) ;
expect ( account . getPaymentAccountPayload ( ) ! . getAustraliaPayidPayload ( ) ! . getExtraInfo ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EXTRA_INFO ) . getValue ( ) ) ;
break ;
2024-06-08 10:50:50 -04:00
case PaymentAccountForm . FormId . CASH_APP :
expect ( account . getPaymentAccountPayload ( ) ! . getCashAppAccountPayload ( ) ! . getEmailOrMobileNrOrCashtag ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR_OR_CASHTAG ) . getValue ( ) ) ;
expect ( account . getTradeCurrenciesList ( ) . length ) . toEqual ( 2 ) ;
expect ( account . getTradeCurrenciesList ( ) . map ( currency = > currency . getCode ( ) ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
case PaymentAccountForm . FormId . PAYPAL :
expect ( account . getPaymentAccountPayload ( ) ! . getPaypalAccountPayload ( ) ! . getEmailOrMobileNrOrUsername ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR_OR_USERNAME ) . getValue ( ) ) ;
expect ( account . getTradeCurrenciesList ( ) . map ( currency = > currency . getCode ( ) ) . join ( "," ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . TRADE_CURRENCIES ) . getValue ( ) ) ;
break ;
case PaymentAccountForm . FormId . VENMO :
expect ( account . getPaymentAccountPayload ( ) ! . getVenmoAccountPayload ( ) ! . getEmailOrMobileNrOrUsername ( ) ) . toEqual ( getFormField ( form , PaymentAccountFormField . FieldId . EMAIL_OR_MOBILE_NR_OR_USERNAME ) . getValue ( ) ) ;
expect ( account . getTradeCurrenciesList ( ) . length ) . toEqual ( 1 ) ;
expect ( account . getTradeCurrenciesList ( ) [ 0 ] . getCode ( ) ) . toEqual ( "USD" ) ;
break ;
2022-06-16 21:51:22 -04:00
default :
throw new Error ( "Unhandled payment method type: " + form . getId ( ) ) ;
}
2022-12-13 08:44:20 +00:00
}