Add API functions to get wallet transfers

This commit is contained in:
l0nelyc0w 2021-12-30 22:03:00 +02:00 committed by woodser
parent ea124b1d57
commit f82baecfbf
2 changed files with 178 additions and 9 deletions

View File

@ -5,12 +5,15 @@ import {HavenoDaemon} from "./HavenoDaemon";
import {HavenoUtils} from "./HavenoUtils";
import * as grpcWeb from 'grpc-web';
import {XmrBalanceInfo, OfferInfo, TradeInfo, MarketPriceInfo} from './protobuf/grpc_pb'; // TODO (woodser): better names; haveno_grpc_pb, haveno_pb
import {PaymentAccount, Offer} from './protobuf/pb_pb';
import {PaymentAccount} from './protobuf/pb_pb';
import {XmrDestination, XmrTx, XmrIncomingTransfer, XmrOutgoingTransfer} from './protobuf/grpc_pb';
// import monero-javascript
const monerojs = require("monero-javascript"); // TODO (woodser): support typescript and `npm install @types/monero-javascript` in monero-javascript
const GenUtils = monerojs.GenUtils;
const MoneroNetworkType = monerojs.MoneroNetworkType;
const MoneroTxConfig = monerojs.MoneroTxConfig;
const MoneroUtils = monerojs.MoneroUtils;
const TaskLooper = monerojs.TaskLooper;
// other required imports
@ -88,6 +91,10 @@ const TestConfig = {
])
};
interface TxContext {
isCreatedTx: boolean;
}
// clients
let arbitrator: HavenoDaemon;
let alice: HavenoDaemon;
@ -188,6 +195,58 @@ test("Can get market prices", async () => {
.toThrow('Currency not found: INVALID_CURRENCY');
});
// test wallet balances, transactions, deposit addresses, create and relay txs
test("Has a Monero wallet", async () => {
// wait for alice to have unlocked balance
let tradeAmount: bigint = BigInt("250000000000");
await waitForUnlockedBalance(tradeAmount * BigInt("2"), alice);
// test balances
let balancesBefore: XmrBalanceInfo = await alice.getBalances(); // TODO: rename to getXmrBalances() for consistency?
expect(BigInt(balancesBefore.getUnlockedBalance())).toBeGreaterThan(BigInt("0"));
expect(BigInt(balancesBefore.getBalance())).toBeGreaterThanOrEqual(BigInt(balancesBefore.getUnlockedBalance()));
// get transactions
let txs: XmrTx[]= await alice.getXmrTxs();
assert(txs.length > 0);
for (let tx of txs) {
testTx(tx, {isCreatedTx: false});
}
// get new deposit addresses
for (let i = 0; i < 0; i++) {
let address = await alice.getNewDepositSubaddress(); // TODO: rename to getNewDepositAddress()
MoneroUtils.validateAddress(address, MoneroNetworkType.STAGNET);
}
// create withdraw tx
let destination = new XmrDestination().setAddress(await alice.getNewDepositSubaddress()).setAmount("100000000000");
let tx = await alice.createXmrTx([destination]);
testTx(tx, {isCreatedTx: true});
// relay withdraw tx
let txHash = await alice.relayXmrTx(tx.getMetadata());
expect(txHash.length).toEqual(64);
// balances decreased
let balancesAfter = await alice.getBalances();
expect(BigInt(balancesAfter.getBalance())).toBeLessThan(BigInt(balancesBefore.getBalance()));
expect(BigInt(balancesAfter.getUnlockedBalance())).toBeLessThan(BigInt(balancesBefore.getUnlockedBalance()));
// get relayed tx
tx = await alice.getXmrTx(txHash);
testTx(tx, {isCreatedTx: false});
// relay invalid tx
try {
await alice.relayXmrTx("invalid tx metadata");
throw new Error("Cannot relay invalid tx metadata");
} catch (err) {
if (err.message !== "Failed to parse hex.") throw new Error("Unexpected error: " + err.message);
}
});
test("Can get balances", async () => {
let balances: XmrBalanceInfo = await alice.getBalances();
expect(BigInt(balances.getUnlockedBalance())).toBeGreaterThanOrEqual(0);
@ -335,7 +394,7 @@ test("Invalidates offers when reserved funds are spent", async () => {
await alice.removeOffer(offer.getId());
throw new Error("cannot remove invalidated offer");
} catch (err) {
if (err.message === "cannot remove invalidated offer") throw new Error(err.message);
if (err.message === "cannot remove invalidated offer") throw new Error("Unexpected error: " + err.message);
}
} catch (err2) {
err = err2;
@ -855,6 +914,62 @@ async function wait(durationMs: number) {
return new Promise(function(resolve) { setTimeout(resolve, durationMs); });
}
function testTx(tx: XmrTx, ctx: TxContext) {
assert(tx.getHash());
expect(BigInt(tx.getFee())).toBeLessThan(TestConfig.maxFee);
if (tx.getIsConfirmed()) {
assert(tx.getTimestamp() > 1000);
assert(tx.getHeight() > 0);
} else {
assert.equal(tx.getHeight(), 0);
}
assert(tx.getOutgoingTransfer() || tx.getIncomingTransfersList().length); // TODO (woodser): test transfers
for (let incomingTransfer of tx.getIncomingTransfersList()) testTransfer(incomingTransfer, ctx);
if (tx.getOutgoingTransfer()) testTransfer(tx.getOutgoingTransfer()!, ctx);
if (ctx.isCreatedTx) testCreatedTx(tx, ctx);
}
function testCreatedTx(tx: XmrTx, ctx: TxContext) {
assert.equal(tx.getTimestamp(), 0);
assert.equal(tx.getIsConfirmed(), false);
assert.equal(tx.getIsLocked(), true);
assert(tx.getMetadata() && tx.getMetadata().length > 0);
}
function testTransfer(transfer: XmrIncomingTransfer|XmrOutgoingTransfer, ctx: TxContext) {
expect(BigInt(transfer.getAmount())).toBeGreaterThanOrEqual(BigInt("0"));
assert(transfer.getAccountIndex() >= 0);
if (transfer instanceof XmrIncomingTransfer) testIncomingTransfer(transfer, ctx);
else testOutgoingTransfer(transfer, ctx);
}
function testIncomingTransfer(transfer: XmrIncomingTransfer, ctx: TxContext) {
assert(transfer.getAddress());
assert(transfer.getSubaddressIndex() >= 0);
assert(transfer.getNumSuggestedConfirmations() > 0);
}
function testOutgoingTransfer(transfer: XmrOutgoingTransfer, ctx: TxContext) {
if (!ctx.isCreatedTx) assert(transfer.getSubaddressIndicesList().length > 0);
for (let subaddressIdx of transfer.getSubaddressIndicesList()) assert(subaddressIdx >= 0);
// test destinations sum to outgoing amount
if (transfer.getDestinationsList().length > 0) {
let sum = BigInt(0);
for (let destination of transfer.getDestinationsList()) {
testDestination(destination);
expect(BigInt(destination.getAmount())).toBeGreaterThan(BigInt("0"));
sum += BigInt(destination.getAmount());
}
assert.equal(sum, BigInt(transfer.getAmount()));
}
}
function testDestination(destination: XmrDestination) {
assert(destination.getAddress());
expect(BigInt(destination.getAmount())).toBeGreaterThan(BigInt("0"));
}
async function createCryptoPaymentAccount(trader: HavenoDaemon): Promise<PaymentAccount> {
let testAccount = TestConfig.cryptoAccounts[0];
let paymentAccount: PaymentAccount = await trader.createCryptoPaymentAccount(
@ -891,7 +1006,6 @@ async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint,
// offer is included in my offers only
if (!getOffer(await maker.getMyOffers(direction), offer.getId())) {
console.log("OK, we couldn't get the offer, let's wait");
await wait(10000);
if (!getOffer(await maker.getMyOffers(direction), offer.getId())) throw new Error("Offer " + offer.getId() + " was not found in my offers");
else console.log("The offer finally posted!");
@ -901,10 +1015,6 @@ async function postOffer(maker: HavenoDaemon, direction: string, amount: bigint,
return offer;
}
function getBalancesStr(balances: XmrBalanceInfo) {
return "[balance=" + balances.getBalance() + ", unlocked balance=" + balances.getUnlockedBalance() + ", locked balance=" + balances.getLockedBalance() + ", reserved offer balance=" + balances.getReservedOfferBalance() + ", reserved trade balance: " + balances.getReservedTradeBalance() + "]";
}
function getOffer(offers: OfferInfo[], id: string): OfferInfo | undefined {
return offers.find(offer => offer.getId() === id);
}

View File

@ -1,7 +1,7 @@
import {HavenoUtils} from "./HavenoUtils";
import * as grpcWeb from 'grpc-web';
import {GetVersionClient, PriceClient, WalletsClient, OffersClient, PaymentAccountsClient, TradesClient} from './protobuf/GrpcServiceClientPb';
import {GetVersionRequest, GetVersionReply, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest} from './protobuf/grpc_pb';
import {GetVersionRequest, GetVersionReply, MarketPriceRequest, MarketPriceReply, MarketPricesRequest, MarketPricesReply, MarketPriceInfo, GetBalancesRequest, GetBalancesReply, XmrBalanceInfo, GetOffersRequest, GetOffersReply, OfferInfo, GetPaymentAccountsRequest, GetPaymentAccountsReply, CreateCryptoCurrencyPaymentAccountRequest, CreateCryptoCurrencyPaymentAccountReply, CreateOfferRequest, CreateOfferReply, CancelOfferRequest, TakeOfferRequest, TakeOfferReply, TradeInfo, GetTradeRequest, GetTradeReply, GetTradesRequest, GetTradesReply, GetNewDepositSubaddressRequest, GetNewDepositSubaddressReply, ConfirmPaymentStartedRequest, ConfirmPaymentReceivedRequest, XmrTx, GetXmrTxsRequest, GetXmrTxsReply, XmrDestination, CreateXmrTxRequest, CreateXmrTxReply, RelayXmrTxRequest, RelayXmrTxReply} from './protobuf/grpc_pb';
import {PaymentAccount, AvailabilityResult} from './protobuf/pb_pb';
const console = require('console');
@ -246,7 +246,7 @@ class HavenoDaemon {
}
/**
* Get a new subaddress in the Haveno wallet to receive deposits.
* Get a new subaddress in the Monero wallet to receive deposits.
*
* @return {string} the deposit address (a subaddress in the Haveno wallet)
*/
@ -259,7 +259,66 @@ class HavenoDaemon {
});
});
}
/**
* Get all transactions in the Monero wallet.
*
* @return {XmrTx[]} the transactions
*/
async getXmrTxs(): Promise<XmrTx[]> {
let that = this;
return new Promise(function(resolve, reject) {
that._walletsClient.getXmrTxs(new GetXmrTxsRequest(), {password: that._password}, function(err: grpcWeb.RpcError, response: GetXmrTxsReply) {
if (err) reject(err);
else resolve(response.getTxsList());
});
});
}
/**
* Get a transaction by hash in the Monero wallet.
*
* @param {String} txHash - hash of the transaction to get
* @return {XmrTx} the transaction with the hash
*/
async getXmrTx(txHash: string): Promise<XmrTx> {
let txs = await this.getXmrTxs(); // TODO (woodser): implement getXmrTx(hash) grpc call
for (let tx of txs) {
if (tx.getHash() === txHash) return tx;
}
throw new Error("No transaction with hash " + txHash);
}
/**
* Create but do not relay a transaction to send funds from the Monero wallet.
*
* @return {XmrTx} the created transaction
*/
async createXmrTx(destinations: XmrDestination[]): Promise<XmrTx> {
let that = this;
return new Promise(function(resolve, reject) {
that._walletsClient.createXmrTx(new CreateXmrTxRequest().setDestinationsList(destinations), {password: that._password}, function(err: grpcWeb.RpcError, response: CreateXmrTxReply) {
if (err) reject(err);
else resolve(response.getTx());
});
});
}
/**
* Relay a previously created transaction to send funds from the Monero wallet.
*
* @return {string} the hash of the relayed transaction
*/
async relayXmrTx(metadata: string): Promise<string> {
let that = this;
return new Promise(function(resolve, reject) {
that._walletsClient.relayXmrTx(new RelayXmrTxRequest().setMetadata(metadata), {password: that._password}, function(err: grpcWeb.RpcError, response: RelayXmrTxReply) {
if (err) reject(err);
else resolve(response.getHash());
});
});
}
/**
* Get payment accounts.
*