deposit from wallet webex: wip
This commit is contained in:
parent
b8200de6f6
commit
2e71117f59
@ -54,6 +54,7 @@ import {
|
|||||||
} from "./talerTypes.js";
|
} from "./talerTypes.js";
|
||||||
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
|
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
|
||||||
import { BackupRecovery } from "./backupTypes.js";
|
import { BackupRecovery } from "./backupTypes.js";
|
||||||
|
import { PaytoUri } from "./payto.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Response for the create reserve request to the wallet.
|
* Response for the create reserve request to the wallet.
|
||||||
@ -525,6 +526,10 @@ export interface ExchangesListRespose {
|
|||||||
exchanges: ExchangeListItem[];
|
exchanges: ExchangeListItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KnownBankAccounts {
|
||||||
|
accounts: PaytoUri[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface ExchangeTos {
|
export interface ExchangeTos {
|
||||||
acceptedVersion?: string;
|
acceptedVersion?: string;
|
||||||
currentVersion?: string;
|
currentVersion?: string;
|
||||||
@ -737,12 +742,19 @@ export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
|
|||||||
export interface GetWithdrawalDetailsForUriRequest {
|
export interface GetWithdrawalDetailsForUriRequest {
|
||||||
talerWithdrawUri: string;
|
talerWithdrawUri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForGetWithdrawalDetailsForUri = (): Codec<GetWithdrawalDetailsForUriRequest> =>
|
export const codecForGetWithdrawalDetailsForUri = (): Codec<GetWithdrawalDetailsForUriRequest> =>
|
||||||
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
|
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
|
||||||
.property("talerWithdrawUri", codecForString())
|
.property("talerWithdrawUri", codecForString())
|
||||||
.build("GetWithdrawalDetailsForUriRequest");
|
.build("GetWithdrawalDetailsForUriRequest");
|
||||||
|
|
||||||
|
export interface ListKnownBankAccountsRequest {
|
||||||
|
currency?: string;
|
||||||
|
}
|
||||||
|
export const codecForListKnownBankAccounts = (): Codec<ListKnownBankAccountsRequest> =>
|
||||||
|
buildCodecForObject<ListKnownBankAccountsRequest>()
|
||||||
|
.property("currency", codecOptional(codecForString()))
|
||||||
|
.build("ListKnownBankAccountsRequest");
|
||||||
|
|
||||||
export interface GetExchangeWithdrawalInfo {
|
export interface GetExchangeWithdrawalInfo {
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
amount: AmountJson;
|
amount: AmountJson;
|
||||||
@ -965,11 +977,23 @@ export const codecForAbortPayWithRefundRequest = (): Codec<AbortPayWithRefundReq
|
|||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
.build("AbortPayWithRefundRequest");
|
.build("AbortPayWithRefundRequest");
|
||||||
|
|
||||||
|
export interface GetFeeForDepositRequest {
|
||||||
|
depositPaytoUri: string;
|
||||||
|
amount: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateDepositGroupRequest {
|
export interface CreateDepositGroupRequest {
|
||||||
depositPaytoUri: string;
|
depositPaytoUri: string;
|
||||||
amount: string;
|
amount: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export const codecForGetFeeForDeposit = (): Codec<GetFeeForDepositRequest> =>
|
||||||
|
buildCodecForObject<GetFeeForDepositRequest>()
|
||||||
|
.property("amount", codecForAmountString())
|
||||||
|
.property("depositPaytoUri", codecForString())
|
||||||
|
.build("GetFeeForDepositRequest");
|
||||||
|
|
||||||
export const codecForCreateDepositGroupRequest = (): Codec<CreateDepositGroupRequest> =>
|
export const codecForCreateDepositGroupRequest = (): Codec<CreateDepositGroupRequest> =>
|
||||||
buildCodecForObject<CreateDepositGroupRequest>()
|
buildCodecForObject<CreateDepositGroupRequest>()
|
||||||
.property("amount", codecForAmountString())
|
.property("amount", codecForAmountString())
|
||||||
|
@ -369,7 +369,7 @@ export class CryptoImplementation {
|
|||||||
sig: string,
|
sig: string,
|
||||||
masterPub: string,
|
masterPub: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (versionCurrent === 10) {
|
if (versionCurrent === 10 || versionCurrent === 11) {
|
||||||
const paytoHash = hash(stringToBytes(paytoUri + "\0"));
|
const paytoHash = hash(stringToBytes(paytoUri + "\0"));
|
||||||
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
|
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
|
||||||
.put(paytoHash)
|
.put(paytoHash)
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AmountJson,
|
||||||
Amounts,
|
Amounts,
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
canonicalJson,
|
canonicalJson,
|
||||||
@ -28,6 +29,7 @@ import {
|
|||||||
decodeCrock,
|
decodeCrock,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
durationFromSpec,
|
durationFromSpec,
|
||||||
|
GetFeeForDepositRequest,
|
||||||
getTimestampNow,
|
getTimestampNow,
|
||||||
Logger,
|
Logger,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@ -35,6 +37,7 @@ import {
|
|||||||
TalerErrorDetails,
|
TalerErrorDetails,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
timestampAddDuration,
|
timestampAddDuration,
|
||||||
|
timestampIsBetween,
|
||||||
timestampTruncateToSecond,
|
timestampTruncateToSecond,
|
||||||
TrackDepositGroupRequest,
|
TrackDepositGroupRequest,
|
||||||
TrackDepositGroupResponse,
|
TrackDepositGroupResponse,
|
||||||
@ -49,7 +52,7 @@ import {
|
|||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { DepositGroupRecord } from "../db.js";
|
import { DepositGroupRecord } from "../db.js";
|
||||||
import { guardOperationException } from "../errors.js";
|
import { guardOperationException } from "../errors.js";
|
||||||
import { selectPayCoins } from "../util/coinSelection.js";
|
import { PayCoinSelection, selectPayCoins } from "../util/coinSelection.js";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
@ -58,11 +61,11 @@ import {
|
|||||||
extractContractData,
|
extractContractData,
|
||||||
generateDepositPermissions,
|
generateDepositPermissions,
|
||||||
getCandidatePayCoins,
|
getCandidatePayCoins,
|
||||||
getEffectiveDepositAmount,
|
|
||||||
getTotalPaymentCost,
|
getTotalPaymentCost,
|
||||||
hashWire,
|
hashWire,
|
||||||
hashWireLegacy,
|
hashWireLegacy,
|
||||||
} from "./pay.js";
|
} from "./pay.js";
|
||||||
|
import { getTotalRefreshCost } from "./refresh.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -342,6 +345,100 @@ export async function trackDepositGroup(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFeeForDeposit(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: GetFeeForDepositRequest,
|
||||||
|
): Promise<DepositFee> {
|
||||||
|
const p = parsePaytoUri(req.depositPaytoUri);
|
||||||
|
if (!p) {
|
||||||
|
throw Error("invalid payto URI");
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = Amounts.parseOrThrow(req.amount);
|
||||||
|
|
||||||
|
const exchangeInfos: { url: string; master_pub: string }[] = [];
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const allExchanges = await tx.exchanges.iter().toArray();
|
||||||
|
for (const e of allExchanges) {
|
||||||
|
const details = await getExchangeDetails(tx, e.baseUrl);
|
||||||
|
if (!details) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
exchangeInfos.push({
|
||||||
|
master_pub: details.masterPublicKey,
|
||||||
|
url: e.baseUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const timestamp = getTimestampNow();
|
||||||
|
const timestampRound = timestampTruncateToSecond(timestamp);
|
||||||
|
// const noncePair = await ws.cryptoApi.createEddsaKeypair();
|
||||||
|
// const merchantPair = await ws.cryptoApi.createEddsaKeypair();
|
||||||
|
// const wireSalt = encodeCrock(getRandomBytes(16));
|
||||||
|
// const wireHash = hashWire(req.depositPaytoUri, wireSalt);
|
||||||
|
// const wireHashLegacy = hashWireLegacy(req.depositPaytoUri, wireSalt);
|
||||||
|
const contractTerms: ContractTerms = {
|
||||||
|
auditors: [],
|
||||||
|
exchanges: exchangeInfos,
|
||||||
|
amount: req.amount,
|
||||||
|
max_fee: Amounts.stringify(amount),
|
||||||
|
max_wire_fee: Amounts.stringify(amount),
|
||||||
|
wire_method: p.targetType,
|
||||||
|
timestamp: timestampRound,
|
||||||
|
merchant_base_url: "",
|
||||||
|
summary: "",
|
||||||
|
nonce: "",
|
||||||
|
wire_transfer_deadline: timestampRound,
|
||||||
|
order_id: "",
|
||||||
|
h_wire: "",
|
||||||
|
pay_deadline: timestampAddDuration(
|
||||||
|
timestampRound,
|
||||||
|
durationFromSpec({ hours: 1 }),
|
||||||
|
),
|
||||||
|
merchant: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
merchant_pub: "",
|
||||||
|
refund_deadline: { t_ms: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const contractData = extractContractData(
|
||||||
|
contractTerms,
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const candidates = await getCandidatePayCoins(ws, contractData);
|
||||||
|
|
||||||
|
const payCoinSel = selectPayCoins({
|
||||||
|
candidates,
|
||||||
|
contractTermsAmount: contractData.amount,
|
||||||
|
depositFeeLimit: contractData.maxDepositFee,
|
||||||
|
wireFeeAmortization: contractData.wireFeeAmortization ?? 1,
|
||||||
|
wireFeeLimit: contractData.maxWireFee,
|
||||||
|
prevPayCoins: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!payCoinSel) {
|
||||||
|
throw Error("insufficient funds");
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getTotalFeeForDepositAmount(
|
||||||
|
ws,
|
||||||
|
p.targetType,
|
||||||
|
amount,
|
||||||
|
payCoinSel,
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
export async function createDepositGroup(
|
export async function createDepositGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: CreateDepositGroupRequest,
|
req: CreateDepositGroupRequest,
|
||||||
@ -495,3 +592,152 @@ export async function createDepositGroup(
|
|||||||
|
|
||||||
return { depositGroupId };
|
return { depositGroupId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the amount that will be deposited on the merchant's bank
|
||||||
|
* account, not considering aggregation.
|
||||||
|
*/
|
||||||
|
export async function getEffectiveDepositAmount(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
wireType: string,
|
||||||
|
pcs: PayCoinSelection,
|
||||||
|
): Promise<AmountJson> {
|
||||||
|
const amt: AmountJson[] = [];
|
||||||
|
const fees: AmountJson[] = [];
|
||||||
|
const exchangeSet: Set<string> = new Set();
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
coins: x.coins,
|
||||||
|
denominations: x.denominations,
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
for (let i = 0; i < pcs.coinPubs.length; i++) {
|
||||||
|
const coin = await tx.coins.get(pcs.coinPubs[i]);
|
||||||
|
if (!coin) {
|
||||||
|
throw Error("can't calculate deposit amount, coin not found");
|
||||||
|
}
|
||||||
|
const denom = await tx.denominations.get([
|
||||||
|
coin.exchangeBaseUrl,
|
||||||
|
coin.denomPubHash,
|
||||||
|
]);
|
||||||
|
if (!denom) {
|
||||||
|
throw Error("can't find denomination to calculate deposit amount");
|
||||||
|
}
|
||||||
|
amt.push(pcs.coinContributions[i]);
|
||||||
|
fees.push(denom.feeDeposit);
|
||||||
|
exchangeSet.add(coin.exchangeBaseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const exchangeUrl of exchangeSet.values()) {
|
||||||
|
const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME/NOTE: the line below _likely_ throws exception
|
||||||
|
// about "find method not found on undefined" when the wireType
|
||||||
|
// is not supported by the Exchange.
|
||||||
|
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
|
||||||
|
return timestampIsBetween(
|
||||||
|
getTimestampNow(),
|
||||||
|
x.startStamp,
|
||||||
|
x.endStamp,
|
||||||
|
);
|
||||||
|
})?.wireFee;
|
||||||
|
if (fee) {
|
||||||
|
fees.push(fee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DepositFee {
|
||||||
|
coin: AmountJson;
|
||||||
|
wire: AmountJson;
|
||||||
|
refresh: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the fee amount that will be charged when trying to deposit the
|
||||||
|
* specified amount using the selected coins and the wire method.
|
||||||
|
*/
|
||||||
|
export async function getTotalFeeForDepositAmount(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
wireType: string,
|
||||||
|
total: AmountJson,
|
||||||
|
pcs: PayCoinSelection,
|
||||||
|
): Promise<DepositFee> {
|
||||||
|
const wireFee: AmountJson[] = [];
|
||||||
|
const coinFee: AmountJson[] = [];
|
||||||
|
const refreshFee: AmountJson[] = [];
|
||||||
|
const exchangeSet: Set<string> = new Set();
|
||||||
|
|
||||||
|
// let acc: AmountJson = Amounts.getZero(total.currency);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
coins: x.coins,
|
||||||
|
denominations: x.denominations,
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
for (let i = 0; i < pcs.coinPubs.length; i++) {
|
||||||
|
const coin = await tx.coins.get(pcs.coinPubs[i]);
|
||||||
|
if (!coin) {
|
||||||
|
throw Error("can't calculate deposit amount, coin not found");
|
||||||
|
}
|
||||||
|
const denom = await tx.denominations.get([
|
||||||
|
coin.exchangeBaseUrl,
|
||||||
|
coin.denomPubHash,
|
||||||
|
]);
|
||||||
|
if (!denom) {
|
||||||
|
throw Error("can't find denomination to calculate deposit amount");
|
||||||
|
}
|
||||||
|
// const cc = pcs.coinContributions[i]
|
||||||
|
// acc = Amounts.add(acc, cc).amount
|
||||||
|
coinFee.push(denom.feeDeposit);
|
||||||
|
exchangeSet.add(coin.exchangeBaseUrl);
|
||||||
|
|
||||||
|
const allDenoms = await tx.denominations.indexes.byExchangeBaseUrl
|
||||||
|
.iter(coin.exchangeBaseUrl)
|
||||||
|
.filter((x) =>
|
||||||
|
Amounts.isSameCurrency(x.value, pcs.coinContributions[i]),
|
||||||
|
);
|
||||||
|
const amountLeft = Amounts.sub(denom.value, pcs.coinContributions[i])
|
||||||
|
.amount;
|
||||||
|
const refreshCost = getTotalRefreshCost(allDenoms, denom, amountLeft);
|
||||||
|
refreshFee.push(refreshCost);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const exchangeUrl of exchangeSet.values()) {
|
||||||
|
const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// FIXME/NOTE: the line below _likely_ throws exception
|
||||||
|
// about "find method not found on undefined" when the wireType
|
||||||
|
// is not supported by the Exchange.
|
||||||
|
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
|
||||||
|
return timestampIsBetween(
|
||||||
|
getTimestampNow(),
|
||||||
|
x.startStamp,
|
||||||
|
x.endStamp,
|
||||||
|
);
|
||||||
|
})?.wireFee;
|
||||||
|
if (fee) {
|
||||||
|
wireFee.push(fee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
coin: coinFee.length === 0 ? Amounts.getZero(total.currency) : Amounts.sum(coinFee).amount,
|
||||||
|
wire: wireFee.length === 0 ? Amounts.getZero(total.currency) : Amounts.sum(wireFee).amount,
|
||||||
|
refresh: refreshFee.length === 0 ? Amounts.getZero(total.currency) : Amounts.sum(refreshFee).amount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -177,66 +177,6 @@ export async function getTotalPaymentCost(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the amount that will be deposited on the merchant's bank
|
|
||||||
* account, not considering aggregation.
|
|
||||||
*/
|
|
||||||
export async function getEffectiveDepositAmount(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
wireType: string,
|
|
||||||
pcs: PayCoinSelection,
|
|
||||||
): Promise<AmountJson> {
|
|
||||||
const amt: AmountJson[] = [];
|
|
||||||
const fees: AmountJson[] = [];
|
|
||||||
const exchangeSet: Set<string> = new Set();
|
|
||||||
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
coins: x.coins,
|
|
||||||
denominations: x.denominations,
|
|
||||||
exchanges: x.exchanges,
|
|
||||||
exchangeDetails: x.exchangeDetails,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
for (let i = 0; i < pcs.coinPubs.length; i++) {
|
|
||||||
const coin = await tx.coins.get(pcs.coinPubs[i]);
|
|
||||||
if (!coin) {
|
|
||||||
throw Error("can't calculate deposit amount, coin not found");
|
|
||||||
}
|
|
||||||
const denom = await tx.denominations.get([
|
|
||||||
coin.exchangeBaseUrl,
|
|
||||||
coin.denomPubHash,
|
|
||||||
]);
|
|
||||||
if (!denom) {
|
|
||||||
throw Error("can't find denomination to calculate deposit amount");
|
|
||||||
}
|
|
||||||
amt.push(pcs.coinContributions[i]);
|
|
||||||
fees.push(denom.feeDeposit);
|
|
||||||
exchangeSet.add(coin.exchangeBaseUrl);
|
|
||||||
}
|
|
||||||
for (const exchangeUrl of exchangeSet.values()) {
|
|
||||||
const exchangeDetails = await getExchangeDetails(tx, exchangeUrl);
|
|
||||||
if (!exchangeDetails) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// FIXME/NOTE: the line below _likely_ throws exception
|
|
||||||
// about "find method not found on undefined" when the wireType
|
|
||||||
// is not supported by the Exchange.
|
|
||||||
const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => {
|
|
||||||
return timestampIsBetween(
|
|
||||||
getTimestampNow(),
|
|
||||||
x.startStamp,
|
|
||||||
x.endStamp,
|
|
||||||
);
|
|
||||||
})?.wireFee;
|
|
||||||
if (fee) {
|
|
||||||
fees.push(fee);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return Amounts.sub(Amounts.sum(amt).amount, Amounts.sum(fees).amount).amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean {
|
function isSpendableCoin(coin: CoinRecord, denom: DenominationRecord): boolean {
|
||||||
if (coin.suspended) {
|
if (coin.suspended) {
|
||||||
return false;
|
return false;
|
||||||
@ -585,8 +525,7 @@ async function incrementPurchasePayRetry(
|
|||||||
pr.payRetryInfo.retryCounter++;
|
pr.payRetryInfo.retryCounter++;
|
||||||
updateRetryInfoTimeout(pr.payRetryInfo);
|
updateRetryInfoTimeout(pr.payRetryInfo);
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`retrying pay in ${
|
`retrying pay in ${getDurationRemaining(pr.payRetryInfo.nextRetry).d_ms
|
||||||
getDurationRemaining(pr.payRetryInfo.nextRetry).d_ms
|
|
||||||
} ms`,
|
} ms`,
|
||||||
);
|
);
|
||||||
pr.lastPayError = err;
|
pr.lastPayError = err;
|
||||||
|
@ -83,6 +83,7 @@ export enum WalletApiOperation {
|
|||||||
AddExchange = "addExchange",
|
AddExchange = "addExchange",
|
||||||
GetTransactions = "getTransactions",
|
GetTransactions = "getTransactions",
|
||||||
ListExchanges = "listExchanges",
|
ListExchanges = "listExchanges",
|
||||||
|
ListKnownBankAccounts = "listKnownBankAccounts",
|
||||||
GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri",
|
GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri",
|
||||||
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
|
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
|
||||||
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
||||||
|
@ -41,6 +41,10 @@ import {
|
|||||||
codecForWithdrawFakebankRequest,
|
codecForWithdrawFakebankRequest,
|
||||||
URL,
|
URL,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
|
KnownBankAccounts,
|
||||||
|
PaytoUri,
|
||||||
|
codecForGetFeeForDeposit,
|
||||||
|
codecForListKnownBankAccounts,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
addBackupProvider,
|
addBackupProvider,
|
||||||
@ -58,6 +62,7 @@ import { exportBackup } from "./operations/backup/export.js";
|
|||||||
import { getBalances } from "./operations/balance.js";
|
import { getBalances } from "./operations/balance.js";
|
||||||
import {
|
import {
|
||||||
createDepositGroup,
|
createDepositGroup,
|
||||||
|
getFeeForDeposit,
|
||||||
processDepositGroup,
|
processDepositGroup,
|
||||||
trackDepositGroup,
|
trackDepositGroup,
|
||||||
} from "./operations/deposits.js";
|
} from "./operations/deposits.js";
|
||||||
@ -495,6 +500,30 @@ async function getExchangeTos(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listKnownBankAccounts(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
currency?: string,
|
||||||
|
): Promise<KnownBankAccounts> {
|
||||||
|
const accounts: PaytoUri[] = []
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
reserves: x.reserves,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
const reservesRecords = await tx.reserves.iter().toArray()
|
||||||
|
for (const r of reservesRecords) {
|
||||||
|
if (currency && currency !== r.currency) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const payto = r.senderWire ? parsePaytoUri(r.senderWire) : undefined
|
||||||
|
if (payto) {
|
||||||
|
accounts.push(payto)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return { accounts }
|
||||||
|
}
|
||||||
|
|
||||||
async function getExchanges(
|
async function getExchanges(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
): Promise<ExchangesListRespose> {
|
): Promise<ExchangesListRespose> {
|
||||||
@ -728,6 +757,10 @@ async function dispatchRequestInternal(
|
|||||||
case "listExchanges": {
|
case "listExchanges": {
|
||||||
return await getExchanges(ws);
|
return await getExchanges(ws);
|
||||||
}
|
}
|
||||||
|
case "listKnownBankAccounts": {
|
||||||
|
const req = codecForListKnownBankAccounts().decode(payload);
|
||||||
|
return await listKnownBankAccounts(ws, req.currency);
|
||||||
|
}
|
||||||
case "getWithdrawalDetailsForUri": {
|
case "getWithdrawalDetailsForUri": {
|
||||||
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
|
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
|
||||||
return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
|
return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
|
||||||
@ -881,6 +914,10 @@ async function dispatchRequestInternal(
|
|||||||
const resp = await getBackupInfo(ws);
|
const resp = await getBackupInfo(ws);
|
||||||
return resp;
|
return resp;
|
||||||
}
|
}
|
||||||
|
case "getFeeForDeposit": {
|
||||||
|
const req = codecForGetFeeForDeposit().decode(payload);
|
||||||
|
return await getFeeForDeposit(ws, req);
|
||||||
|
}
|
||||||
case "createDepositGroup": {
|
case "createDepositGroup": {
|
||||||
const req = codecForCreateDepositGroupRequest().decode(payload);
|
const req = codecForCreateDepositGroupRequest().decode(payload);
|
||||||
return await createDepositGroup(ws, req);
|
return await createDepositGroup(ws, req);
|
||||||
|
@ -34,6 +34,7 @@ export enum Pages {
|
|||||||
welcome = "/welcome",
|
welcome = "/welcome",
|
||||||
balance = "/balance",
|
balance = "/balance",
|
||||||
manual_withdraw = "/manual-withdraw",
|
manual_withdraw = "/manual-withdraw",
|
||||||
|
deposit = "/deposit/:currency",
|
||||||
settings = "/settings",
|
settings = "/settings",
|
||||||
dev = "/dev",
|
dev = "/dev",
|
||||||
cta = "/cta",
|
cta = "/cta",
|
||||||
|
@ -16,9 +16,18 @@
|
|||||||
|
|
||||||
import { amountFractionalBase, Amounts, Balance } from "@gnu-taler/taler-util";
|
import { amountFractionalBase, Amounts, Balance } from "@gnu-taler/taler-util";
|
||||||
import { h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { TableWithRoundRows as TableWithRoundedRows } from "./styled/index";
|
import {
|
||||||
|
ButtonPrimary,
|
||||||
|
TableWithRoundRows as TableWithRoundedRows,
|
||||||
|
} from "./styled/index";
|
||||||
|
|
||||||
export function BalanceTable({ balances }: { balances: Balance[] }): VNode {
|
export function BalanceTable({
|
||||||
|
balances,
|
||||||
|
goToWalletDeposit,
|
||||||
|
}: {
|
||||||
|
balances: Balance[];
|
||||||
|
goToWalletDeposit: (currency: string) => void;
|
||||||
|
}): VNode {
|
||||||
const currencyFormatter = new Intl.NumberFormat("en-US");
|
const currencyFormatter = new Intl.NumberFormat("en-US");
|
||||||
return (
|
return (
|
||||||
<TableWithRoundedRows>
|
<TableWithRoundedRows>
|
||||||
@ -40,6 +49,11 @@ export function BalanceTable({ balances }: { balances: Balance[] }): VNode {
|
|||||||
>
|
>
|
||||||
{v}
|
{v}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<ButtonPrimary onClick={() => goToWalletDeposit(av.currency)}>
|
||||||
|
Deposit
|
||||||
|
</ButtonPrimary>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
@ -716,6 +716,10 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ErrorText = styled.div`
|
||||||
|
color: red;
|
||||||
|
`;
|
||||||
|
|
||||||
export const ErrorBox = styled.div`
|
export const ErrorBox = styled.div`
|
||||||
border: 2px solid #f5c6cb;
|
border: 2px solid #f5c6cb;
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
|
@ -21,18 +21,21 @@ import { ButtonPrimary, ErrorBox } from "../components/styled/index";
|
|||||||
import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||||
import { PageLink } from "../renderHtml";
|
import { PageLink } from "../renderHtml";
|
||||||
import * as wxApi from "../wxApi";
|
import * as wxApi from "../wxApi";
|
||||||
|
interface Props {
|
||||||
|
goToWalletDeposit: (currency: string) => void;
|
||||||
|
goToWalletManualWithdraw: () => void;
|
||||||
|
}
|
||||||
export function BalancePage({
|
export function BalancePage({
|
||||||
goToWalletManualWithdraw,
|
goToWalletManualWithdraw,
|
||||||
}: {
|
goToWalletDeposit,
|
||||||
goToWalletManualWithdraw: () => void;
|
}: Props): VNode {
|
||||||
}): VNode {
|
|
||||||
const state = useAsyncAsHook(wxApi.getBalance);
|
const state = useAsyncAsHook(wxApi.getBalance);
|
||||||
return (
|
return (
|
||||||
<BalanceView
|
<BalanceView
|
||||||
balance={state}
|
balance={state}
|
||||||
Linker={PageLink}
|
Linker={PageLink}
|
||||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||||
|
goToWalletDeposit={goToWalletDeposit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -40,12 +43,14 @@ export interface BalanceViewProps {
|
|||||||
balance: HookResponse<BalancesResponse>;
|
balance: HookResponse<BalancesResponse>;
|
||||||
Linker: typeof PageLink;
|
Linker: typeof PageLink;
|
||||||
goToWalletManualWithdraw: () => void;
|
goToWalletManualWithdraw: () => void;
|
||||||
|
goToWalletDeposit: (currency: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BalanceView({
|
export function BalanceView({
|
||||||
balance,
|
balance,
|
||||||
Linker,
|
Linker,
|
||||||
goToWalletManualWithdraw,
|
goToWalletManualWithdraw,
|
||||||
|
goToWalletDeposit,
|
||||||
}: BalanceViewProps): VNode {
|
}: BalanceViewProps): VNode {
|
||||||
if (!balance) {
|
if (!balance) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
@ -71,7 +76,8 @@ export function BalanceView({
|
|||||||
<Linker pageName="/welcome">help</Linker> getting started?
|
<Linker pageName="/welcome">help</Linker> getting started?
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
</p>
|
</p>
|
||||||
<footer style={{ justifyContent: "space-around" }}>
|
<footer style={{ justifyContent: "space-between" }}>
|
||||||
|
<div />
|
||||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||||
Withdraw
|
Withdraw
|
||||||
</ButtonPrimary>
|
</ButtonPrimary>
|
||||||
@ -83,9 +89,13 @@ export function BalanceView({
|
|||||||
return (
|
return (
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<section>
|
<section>
|
||||||
<BalanceTable balances={balance.response.balances} />
|
<BalanceTable
|
||||||
|
balances={balance.response.balances}
|
||||||
|
goToWalletDeposit={goToWalletDeposit}
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
<footer style={{ justifyContent: "space-around" }}>
|
<footer style={{ justifyContent: "space-between" }}>
|
||||||
|
<div />
|
||||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||||
Withdraw
|
Withdraw
|
||||||
</ButtonPrimary>
|
</ButtonPrimary>
|
||||||
|
@ -43,14 +43,17 @@ export function DeveloperPage(): VNode {
|
|||||||
? []
|
? []
|
||||||
: operationsResponse.response.pendingOperations;
|
: operationsResponse.response.pendingOperations;
|
||||||
|
|
||||||
return <View status={status}
|
return (
|
||||||
|
<View
|
||||||
|
status={status}
|
||||||
timedOut={timedOut}
|
timedOut={timedOut}
|
||||||
operations={operations}
|
operations={operations}
|
||||||
onDownloadDatabase={async () => {
|
onDownloadDatabase={async () => {
|
||||||
const db = await wxApi.exportDB()
|
const db = await wxApi.exportDB();
|
||||||
return JSON.stringify(db)
|
return JSON.stringify(db);
|
||||||
}}
|
}}
|
||||||
/>;
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
@ -64,14 +67,21 @@ function hashObjectId(o: any): string {
|
|||||||
return JSON.stringify(o);
|
return JSON.stringify(o);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function View({ status, timedOut, operations, onDownloadDatabase }: Props): VNode {
|
export function View({
|
||||||
const [downloadedDatabase, setDownloadedDatabase] = useState<{time: Date; content: string}|undefined>(undefined)
|
status,
|
||||||
|
timedOut,
|
||||||
|
operations,
|
||||||
|
onDownloadDatabase,
|
||||||
|
}: Props): VNode {
|
||||||
|
const [downloadedDatabase, setDownloadedDatabase] = useState<
|
||||||
|
{ time: Date; content: string } | undefined
|
||||||
|
>(undefined);
|
||||||
async function onExportDatabase(): Promise<void> {
|
async function onExportDatabase(): Promise<void> {
|
||||||
const content = await onDownloadDatabase()
|
const content = await onDownloadDatabase();
|
||||||
setDownloadedDatabase({
|
setDownloadedDatabase({
|
||||||
time: new Date(),
|
time: new Date(),
|
||||||
content
|
content,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -83,9 +93,27 @@ export function View({ status, timedOut, operations, onDownloadDatabase }: Props
|
|||||||
<button onClick={confirmReset}>reset</button>
|
<button onClick={confirmReset}>reset</button>
|
||||||
<br />
|
<br />
|
||||||
<button onClick={onExportDatabase}>export database</button>
|
<button onClick={onExportDatabase}>export database</button>
|
||||||
{downloadedDatabase && <div>
|
{downloadedDatabase && (
|
||||||
Database exported at <Time timestamp={{t_ms: downloadedDatabase.time.getTime()}} format="yyyy/MM/dd HH:mm:ss" /> <a href={`data:text/plain;charset=utf-8;base64,${btoa(downloadedDatabase.content)}`} download={`taler-wallet-database-${format(downloadedDatabase.time, 'yyyy/MM/dd_HH:mm')}.json`}>click here</a> to download
|
<div>
|
||||||
</div>}
|
Database exported at
|
||||||
|
<Time
|
||||||
|
timestamp={{ t_ms: downloadedDatabase.time.getTime() }}
|
||||||
|
format="yyyy/MM/dd HH:mm:ss"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
href={`data:text/plain;charset=utf-8;base64,${toBase64(
|
||||||
|
downloadedDatabase.content,
|
||||||
|
)}`}
|
||||||
|
download={`taler-wallet-database-${format(
|
||||||
|
downloadedDatabase.time,
|
||||||
|
"yyyy/MM/dd_HH:mm",
|
||||||
|
)}.json`}
|
||||||
|
>
|
||||||
|
click here
|
||||||
|
</a>
|
||||||
|
to download
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<br />
|
<br />
|
||||||
<Diagnostics diagnostics={status} timedOut={timedOut} />
|
<Diagnostics diagnostics={status} timedOut={timedOut} />
|
||||||
{operations && operations.length > 0 && (
|
{operations && operations.length > 0 && (
|
||||||
@ -109,6 +137,14 @@ export function View({ status, timedOut, operations, onDownloadDatabase }: Props
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toBase64(str: string): string {
|
||||||
|
return btoa(
|
||||||
|
encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
|
||||||
|
return String.fromCharCode(parseInt(p1, 16));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function reload(): void {
|
export function reload(): void {
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line no-undef
|
// eslint-disable-next-line no-undef
|
||||||
|
@ -84,6 +84,9 @@ function Application() {
|
|||||||
goToWalletManualWithdraw={() =>
|
goToWalletManualWithdraw={() =>
|
||||||
goToWalletPage(Pages.manual_withdraw)
|
goToWalletPage(Pages.manual_withdraw)
|
||||||
}
|
}
|
||||||
|
goToWalletDeposit={(currency: string) =>
|
||||||
|
goToWalletPage(Pages.deposit.replace(":currency", currency))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Route path={Pages.settings} component={SettingsPage} />
|
<Route path={Pages.settings} component={SettingsPage} />
|
||||||
<Route
|
<Route
|
||||||
@ -107,6 +110,7 @@ function Application() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path={Pages.history} component={HistoryPage} />
|
<Route path={Pages.history} component={HistoryPage} />
|
||||||
|
|
||||||
<Route
|
<Route
|
||||||
path={Pages.backup}
|
path={Pages.backup}
|
||||||
component={BackupPage}
|
component={BackupPage}
|
||||||
|
@ -24,7 +24,9 @@ import * as wxApi from "../wxApi";
|
|||||||
|
|
||||||
export function BalancePage({
|
export function BalancePage({
|
||||||
goToWalletManualWithdraw,
|
goToWalletManualWithdraw,
|
||||||
|
goToWalletDeposit,
|
||||||
}: {
|
}: {
|
||||||
|
goToWalletDeposit: (currency: string) => void;
|
||||||
goToWalletManualWithdraw: () => void;
|
goToWalletManualWithdraw: () => void;
|
||||||
}): VNode {
|
}): VNode {
|
||||||
const state = useAsyncAsHook(wxApi.getBalance);
|
const state = useAsyncAsHook(wxApi.getBalance);
|
||||||
@ -33,6 +35,7 @@ export function BalancePage({
|
|||||||
balance={state}
|
balance={state}
|
||||||
Linker={PageLink}
|
Linker={PageLink}
|
||||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||||
|
goToWalletDeposit={goToWalletDeposit}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -41,12 +44,14 @@ export interface BalanceViewProps {
|
|||||||
balance: HookResponse<BalancesResponse>;
|
balance: HookResponse<BalancesResponse>;
|
||||||
Linker: typeof PageLink;
|
Linker: typeof PageLink;
|
||||||
goToWalletManualWithdraw: () => void;
|
goToWalletManualWithdraw: () => void;
|
||||||
|
goToWalletDeposit: (currency: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function BalanceView({
|
export function BalanceView({
|
||||||
balance,
|
balance,
|
||||||
Linker,
|
Linker,
|
||||||
goToWalletManualWithdraw,
|
goToWalletManualWithdraw,
|
||||||
|
goToWalletDeposit,
|
||||||
}: BalanceViewProps): VNode {
|
}: BalanceViewProps): VNode {
|
||||||
if (!balance) {
|
if (!balance) {
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
@ -65,28 +70,35 @@ export function BalanceView({
|
|||||||
}
|
}
|
||||||
if (balance.response.balances.length === 0) {
|
if (balance.response.balances.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
<Fragment>
|
||||||
<p>
|
<p>
|
||||||
<Centered style={{ marginTop: 100 }}>
|
<Centered style={{ marginTop: 100 }}>
|
||||||
<i18n.Translate>
|
<i18n.Translate>
|
||||||
You have no balance to show. Need some{" "}
|
You have no balance to show. Need some{" "}
|
||||||
<Linker pageName="/welcome">help</Linker> getting started?
|
<Linker pageName="/welcome">help</Linker> getting started?
|
||||||
</i18n.Translate>
|
</i18n.Translate>
|
||||||
<div>
|
|
||||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
|
||||||
Withdraw
|
|
||||||
</ButtonPrimary>
|
|
||||||
</div>
|
|
||||||
</Centered>
|
</Centered>
|
||||||
</p>
|
</p>
|
||||||
);
|
<footer style={{ justifyContent: "space-between" }}>
|
||||||
}
|
<div />
|
||||||
|
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||||
return (
|
Withdraw
|
||||||
<Fragment>
|
</ButtonPrimary>
|
||||||
<section>
|
</footer>
|
||||||
<BalanceTable balances={balance.response.balances} />
|
</Fragment>
|
||||||
</section>
|
);
|
||||||
<footer style={{ justifyContent: "space-around" }}>
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<section>
|
||||||
|
<BalanceTable
|
||||||
|
balances={balance.response.balances}
|
||||||
|
goToWalletDeposit={goToWalletDeposit}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
<footer style={{ justifyContent: "space-between" }}>
|
||||||
|
<div />
|
||||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||||
Withdraw
|
Withdraw
|
||||||
</ButtonPrimary>
|
</ButtonPrimary>
|
||||||
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2021 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @author Sebastian Javier Marchano (sebasjm)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AmountJson, Amounts, parsePaytoUri } from "@gnu-taler/taler-util";
|
||||||
|
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||||
|
import { createExample } from "../test-utils";
|
||||||
|
import { View as TestedComponent } from "./DepositPage";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "wallet/deposit",
|
||||||
|
component: TestedComponent,
|
||||||
|
argTypes: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function alwaysReturnFeeToOne(): Promise<DepositFee> {
|
||||||
|
const fee = {
|
||||||
|
currency: "EUR",
|
||||||
|
value: 1,
|
||||||
|
fraction: 0,
|
||||||
|
};
|
||||||
|
return { coin: fee, refresh: fee, wire: fee };
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WithEmptyAccountList = createExample(TestedComponent, {
|
||||||
|
knownBankAccounts: [],
|
||||||
|
balance: Amounts.parseOrThrow("USD:10"),
|
||||||
|
onCalculateFee: alwaysReturnFeeToOne,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const WithSomeBankAccounts = createExample(TestedComponent, {
|
||||||
|
knownBankAccounts: [parsePaytoUri("payto://iban/ES8877998399652238")!],
|
||||||
|
balance: Amounts.parseOrThrow("EUR:10"),
|
||||||
|
onCalculateFee: alwaysReturnFeeToOne,
|
||||||
|
});
|
234
packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
Normal file
234
packages/taler-wallet-webextension/src/wallet/DepositPage.tsx
Normal file
@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
This file is part of TALER
|
||||||
|
(C) 2016 GNUnet e.V.
|
||||||
|
|
||||||
|
TALER is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
TALER is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
AmountJson,
|
||||||
|
Amounts,
|
||||||
|
AmountString,
|
||||||
|
PaytoUri,
|
||||||
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||||
|
import { Fragment, h, VNode } from "preact";
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { Part } from "../components/Part";
|
||||||
|
import { SelectList } from "../components/SelectList";
|
||||||
|
import {
|
||||||
|
ButtonPrimary,
|
||||||
|
ErrorText,
|
||||||
|
Input,
|
||||||
|
InputWithLabel,
|
||||||
|
} from "../components/styled";
|
||||||
|
import { useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||||
|
import * as wxApi from "../wxApi";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
currency: string;
|
||||||
|
}
|
||||||
|
export function DepositPage({ currency }: Props): VNode {
|
||||||
|
const [success, setSuccess] = useState(false);
|
||||||
|
|
||||||
|
const state = useAsyncAsHook(async () => {
|
||||||
|
const balance = await wxApi.getBalance();
|
||||||
|
const bs = balance.balances.filter((b) => b.available.startsWith(currency));
|
||||||
|
const currencyBalance =
|
||||||
|
bs.length === 0
|
||||||
|
? Amounts.getZero(currency)
|
||||||
|
: Amounts.parseOrThrow(bs[0].available);
|
||||||
|
const knownAccounts = await wxApi.listKnownBankAccounts(currency);
|
||||||
|
return { accounts: knownAccounts.accounts, currencyBalance };
|
||||||
|
});
|
||||||
|
|
||||||
|
const accounts =
|
||||||
|
state === undefined ? [] : state.hasError ? [] : state.response.accounts;
|
||||||
|
|
||||||
|
const currencyBalance =
|
||||||
|
state === undefined
|
||||||
|
? Amounts.getZero(currency)
|
||||||
|
: state.hasError
|
||||||
|
? Amounts.getZero(currency)
|
||||||
|
: state.response.currencyBalance;
|
||||||
|
|
||||||
|
async function doSend(account: string, amount: AmountString): Promise<void> {
|
||||||
|
await wxApi.createDepositGroup(account, amount);
|
||||||
|
setSuccess(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getFeeForAmount(
|
||||||
|
account: string,
|
||||||
|
amount: AmountString,
|
||||||
|
): Promise<DepositFee> {
|
||||||
|
return await wxApi.getFeeForDeposit(account, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (accounts.length === 0) return <div>loading..</div>;
|
||||||
|
if (success) return <div>deposit created</div>;
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
knownBankAccounts={accounts}
|
||||||
|
balance={currencyBalance}
|
||||||
|
onSend={doSend}
|
||||||
|
onCalculateFee={getFeeForAmount}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ViewProps {
|
||||||
|
knownBankAccounts: Array<PaytoUri>;
|
||||||
|
balance: AmountJson;
|
||||||
|
onSend: (account: string, amount: AmountString) => Promise<void>;
|
||||||
|
onCalculateFee: (
|
||||||
|
account: string,
|
||||||
|
amount: AmountString,
|
||||||
|
) => Promise<DepositFee>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function View({
|
||||||
|
knownBankAccounts,
|
||||||
|
balance,
|
||||||
|
onSend,
|
||||||
|
onCalculateFee,
|
||||||
|
}: ViewProps): VNode {
|
||||||
|
const accountMap = createLabelsForBankAccount(knownBankAccounts);
|
||||||
|
const [accountIdx, setAccountIdx] = useState(0);
|
||||||
|
const [amount, setAmount] = useState<number | undefined>(undefined);
|
||||||
|
const [fee, setFee] = useState<DepositFee | undefined>(undefined);
|
||||||
|
const currency = balance.currency;
|
||||||
|
const amountStr: AmountString = `${currency}:${amount}`;
|
||||||
|
|
||||||
|
const account = knownBankAccounts[accountIdx];
|
||||||
|
const accountURI = `payto://${account.targetType}/${account.targetPath}`;
|
||||||
|
useEffect(() => {
|
||||||
|
if (amount === undefined) return;
|
||||||
|
onCalculateFee(accountURI, amountStr).then((result) => {
|
||||||
|
setFee(result);
|
||||||
|
});
|
||||||
|
}, [amount]);
|
||||||
|
|
||||||
|
if (!balance) {
|
||||||
|
return <div>no balance</div>;
|
||||||
|
}
|
||||||
|
if (!knownBankAccounts || !knownBankAccounts.length) {
|
||||||
|
return <div>there is no known bank account to send money to</div>;
|
||||||
|
}
|
||||||
|
const parsedAmount =
|
||||||
|
amount === undefined ? undefined : Amounts.parse(amountStr);
|
||||||
|
const isDirty = amount !== 0;
|
||||||
|
const error = !isDirty
|
||||||
|
? undefined
|
||||||
|
: !parsedAmount
|
||||||
|
? "Invalid amount"
|
||||||
|
: Amounts.cmp(balance, parsedAmount) === -1
|
||||||
|
? `To much, your current balance is ${balance.value}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<h2>Send {currency} to your account</h2>
|
||||||
|
<section>
|
||||||
|
<Input>
|
||||||
|
<SelectList
|
||||||
|
label="Bank account IBAN number"
|
||||||
|
list={accountMap}
|
||||||
|
name="account"
|
||||||
|
value={String(accountIdx)}
|
||||||
|
onChange={(s) => setAccountIdx(parseInt(s, 10))}
|
||||||
|
/>
|
||||||
|
</Input>
|
||||||
|
<InputWithLabel invalid={!!error}>
|
||||||
|
<label>Amount to send</label>
|
||||||
|
<div>
|
||||||
|
<span>{currency}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={amount}
|
||||||
|
onInput={(e) => {
|
||||||
|
const num = parseFloat(e.currentTarget.value);
|
||||||
|
console.log(num);
|
||||||
|
if (!Number.isNaN(num)) {
|
||||||
|
setAmount(num);
|
||||||
|
} else {
|
||||||
|
setAmount(undefined);
|
||||||
|
setFee(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && <ErrorText>{error}</ErrorText>}
|
||||||
|
</InputWithLabel>
|
||||||
|
{!error && fee && (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<Part
|
||||||
|
title="Exchange fee"
|
||||||
|
text={Amounts.stringify(Amounts.sum([fee.wire, fee.coin]).amount)}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
<Part
|
||||||
|
title="Change cost"
|
||||||
|
text={Amounts.stringify(fee.refresh)}
|
||||||
|
kind="negative"
|
||||||
|
/>
|
||||||
|
{parsedAmount && (
|
||||||
|
<Part
|
||||||
|
title="Total received"
|
||||||
|
text={Amounts.stringify(
|
||||||
|
Amounts.sub(
|
||||||
|
parsedAmount,
|
||||||
|
Amounts.sum([fee.wire, fee.coin]).amount,
|
||||||
|
).amount,
|
||||||
|
)}
|
||||||
|
kind="positive"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
<footer>
|
||||||
|
<div />
|
||||||
|
<ButtonPrimary
|
||||||
|
disabled={!parsedAmount}
|
||||||
|
onClick={() => onSend(accountURI, amountStr)}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</ButtonPrimary>
|
||||||
|
</footer>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createLabelsForBankAccount(knownBankAccounts: Array<PaytoUri>): {
|
||||||
|
[label: number]: string;
|
||||||
|
} {
|
||||||
|
if (!knownBankAccounts) return {};
|
||||||
|
return knownBankAccounts.reduce((prev, cur, i) => {
|
||||||
|
let label = cur.targetPath;
|
||||||
|
if (cur.isKnown) {
|
||||||
|
switch (cur.targetType) {
|
||||||
|
case "x-taler-bank": {
|
||||||
|
label = cur.account;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "iban": {
|
||||||
|
label = cur.iban;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[i]: label,
|
||||||
|
};
|
||||||
|
}, {} as { [label: number]: string });
|
||||||
|
}
|
@ -369,8 +369,8 @@ export function TransactionView({
|
|||||||
|
|
||||||
if (transaction.type === TransactionType.Deposit) {
|
if (transaction.type === TransactionType.Deposit) {
|
||||||
const fee = Amounts.sub(
|
const fee = Amounts.sub(
|
||||||
Amounts.parseOrThrow(transaction.amountRaw),
|
|
||||||
Amounts.parseOrThrow(transaction.amountEffective),
|
Amounts.parseOrThrow(transaction.amountEffective),
|
||||||
|
Amounts.parseOrThrow(transaction.amountRaw),
|
||||||
).amount;
|
).amount;
|
||||||
return (
|
return (
|
||||||
<TransactionTemplate>
|
<TransactionTemplate>
|
||||||
@ -379,15 +379,15 @@ export function TransactionView({
|
|||||||
<br />
|
<br />
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title="Total deposit"
|
title="Total send"
|
||||||
text={amountToString(transaction.amountEffective)}
|
text={amountToString(transaction.amountEffective)}
|
||||||
kind="negative"
|
kind="neutral"
|
||||||
/>
|
/>
|
||||||
<Part
|
<Part
|
||||||
big
|
big
|
||||||
title="Purchase amount"
|
title="Deposit amount"
|
||||||
text={amountToString(transaction.amountRaw)}
|
text={amountToString(transaction.amountRaw)}
|
||||||
kind="neutral"
|
kind="positive"
|
||||||
/>
|
/>
|
||||||
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
|
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
|
||||||
</TransactionTemplate>
|
</TransactionTemplate>
|
||||||
|
@ -45,6 +45,7 @@ import { WalletBox } from "./components/styled";
|
|||||||
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
|
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
|
||||||
import { ProviderAddPage } from "./wallet/ProviderAddPage";
|
import { ProviderAddPage } from "./wallet/ProviderAddPage";
|
||||||
import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
|
import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
|
||||||
|
import { DepositPage } from "./wallet/DepositPage";
|
||||||
|
|
||||||
function main(): void {
|
function main(): void {
|
||||||
try {
|
try {
|
||||||
@ -105,6 +106,9 @@ function Application(): VNode {
|
|||||||
path={Pages.balance}
|
path={Pages.balance}
|
||||||
component={withLogoAndNavBar(BalancePage)}
|
component={withLogoAndNavBar(BalancePage)}
|
||||||
goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
|
goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
|
||||||
|
goToWalletDeposit={(currency: string) =>
|
||||||
|
route(Pages.deposit.replace(":currency", currency))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={Pages.settings}
|
path={Pages.settings}
|
||||||
@ -145,6 +149,10 @@ function Application(): VNode {
|
|||||||
component={withLogoAndNavBar(ManualWithdrawPage)}
|
component={withLogoAndNavBar(ManualWithdrawPage)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path={Pages.deposit}
|
||||||
|
component={withLogoAndNavBar(DepositPage)}
|
||||||
|
/>
|
||||||
<Route
|
<Route
|
||||||
path={Pages.reset_required}
|
path={Pages.reset_required}
|
||||||
component={() => <div>no yet implemented</div>}
|
component={() => <div>no yet implemented</div>}
|
||||||
|
@ -24,10 +24,11 @@
|
|||||||
import {
|
import {
|
||||||
AcceptExchangeTosRequest,
|
AcceptExchangeTosRequest,
|
||||||
AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse,
|
AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse,
|
||||||
AddExchangeRequest, ApplyRefundResponse, BalancesResponse, ConfirmPayResult,
|
AddExchangeRequest, AmountJson, AmountString, ApplyRefundResponse, BalancesResponse, ConfirmPayResult,
|
||||||
CoreApiResponse, DeleteTransactionRequest, ExchangesListRespose,
|
CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, DeleteTransactionRequest, ExchangesListRespose,
|
||||||
GetExchangeTosResult, GetExchangeWithdrawalInfo,
|
GetExchangeTosResult, GetExchangeWithdrawalInfo,
|
||||||
GetWithdrawalDetailsForUriRequest, NotificationType, PreparePayResult, PrepareTipRequest,
|
GetFeeForDepositRequest,
|
||||||
|
GetWithdrawalDetailsForUriRequest, KnownBankAccounts, NotificationType, PreparePayResult, PrepareTipRequest,
|
||||||
PrepareTipResult, RetryTransactionRequest,
|
PrepareTipResult, RetryTransactionRequest,
|
||||||
SetWalletDeviceIdRequest, TransactionsResponse, WalletDiagnostics, WithdrawUriInfoResponse
|
SetWalletDeviceIdRequest, TransactionsResponse, WalletDiagnostics, WithdrawUriInfoResponse
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
@ -36,6 +37,7 @@ import {
|
|||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
RemoveBackupProviderRequest
|
RemoveBackupProviderRequest
|
||||||
} from "@gnu-taler/taler-wallet-core";
|
} from "@gnu-taler/taler-wallet-core";
|
||||||
|
import { DepositFee } from "@gnu-taler/taler-wallet-core/src/operations/deposits";
|
||||||
import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
import { ExchangeWithdrawDetails } from "@gnu-taler/taler-wallet-core/src/operations/withdraw";
|
||||||
import { MessageFromBackend } from "./wxBackend.js";
|
import { MessageFromBackend } from "./wxBackend.js";
|
||||||
|
|
||||||
@ -119,6 +121,18 @@ export function resetDb(): Promise<void> {
|
|||||||
return callBackend("reset-db", {});
|
return callBackend("reset-db", {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFeeForDeposit(depositPaytoUri: string, amount: AmountString): Promise<DepositFee> {
|
||||||
|
return callBackend("getFeeForDeposit", {
|
||||||
|
depositPaytoUri, amount
|
||||||
|
} as GetFeeForDepositRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createDepositGroup(depositPaytoUri: string, amount: AmountString): Promise<CreateDepositGroupResponse> {
|
||||||
|
return callBackend("createDepositGroup", {
|
||||||
|
depositPaytoUri, amount
|
||||||
|
} as CreateDepositGroupRequest);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get balances for all currencies/exchanges.
|
* Get balances for all currencies/exchanges.
|
||||||
*/
|
*/
|
||||||
@ -170,6 +184,9 @@ export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> {
|
|||||||
export function listExchanges(): Promise<ExchangesListRespose> {
|
export function listExchanges(): Promise<ExchangesListRespose> {
|
||||||
return callBackend("listExchanges", {});
|
return callBackend("listExchanges", {});
|
||||||
}
|
}
|
||||||
|
export function listKnownBankAccounts(currency?: string): Promise<KnownBankAccounts> {
|
||||||
|
return callBackend("listKnownBankAccounts", { currency });
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get information about the current state of wallet backups.
|
* Get information about the current state of wallet backups.
|
||||||
|
Loading…
Reference in New Issue
Block a user