deposit from wallet webex: wip
This commit is contained in:
parent
b8200de6f6
commit
2e71117f59
@ -54,6 +54,7 @@ import {
|
||||
} from "./talerTypes.js";
|
||||
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
|
||||
import { BackupRecovery } from "./backupTypes.js";
|
||||
import { PaytoUri } from "./payto.js";
|
||||
|
||||
/**
|
||||
* Response for the create reserve request to the wallet.
|
||||
@ -525,6 +526,10 @@ export interface ExchangesListRespose {
|
||||
exchanges: ExchangeListItem[];
|
||||
}
|
||||
|
||||
export interface KnownBankAccounts {
|
||||
accounts: PaytoUri[];
|
||||
}
|
||||
|
||||
export interface ExchangeTos {
|
||||
acceptedVersion?: string;
|
||||
currentVersion?: string;
|
||||
@ -737,12 +742,19 @@ export const codecForApplyRefundRequest = (): Codec<ApplyRefundRequest> =>
|
||||
export interface GetWithdrawalDetailsForUriRequest {
|
||||
talerWithdrawUri: string;
|
||||
}
|
||||
|
||||
export const codecForGetWithdrawalDetailsForUri = (): Codec<GetWithdrawalDetailsForUriRequest> =>
|
||||
buildCodecForObject<GetWithdrawalDetailsForUriRequest>()
|
||||
.property("talerWithdrawUri", codecForString())
|
||||
.build("GetWithdrawalDetailsForUriRequest");
|
||||
|
||||
export interface ListKnownBankAccountsRequest {
|
||||
currency?: string;
|
||||
}
|
||||
export const codecForListKnownBankAccounts = (): Codec<ListKnownBankAccountsRequest> =>
|
||||
buildCodecForObject<ListKnownBankAccountsRequest>()
|
||||
.property("currency", codecOptional(codecForString()))
|
||||
.build("ListKnownBankAccountsRequest");
|
||||
|
||||
export interface GetExchangeWithdrawalInfo {
|
||||
exchangeBaseUrl: string;
|
||||
amount: AmountJson;
|
||||
@ -965,11 +977,23 @@ export const codecForAbortPayWithRefundRequest = (): Codec<AbortPayWithRefundReq
|
||||
.property("proposalId", codecForString())
|
||||
.build("AbortPayWithRefundRequest");
|
||||
|
||||
export interface GetFeeForDepositRequest {
|
||||
depositPaytoUri: string;
|
||||
amount: AmountString;
|
||||
}
|
||||
|
||||
export interface CreateDepositGroupRequest {
|
||||
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> =>
|
||||
buildCodecForObject<CreateDepositGroupRequest>()
|
||||
.property("amount", codecForAmountString())
|
||||
|
@ -162,7 +162,7 @@ async function myEddsaSign(
|
||||
export class CryptoImplementation {
|
||||
static enableTracing = false;
|
||||
|
||||
constructor(private primitiveWorker?: PrimitiveWorker) {}
|
||||
constructor(private primitiveWorker?: PrimitiveWorker) { }
|
||||
|
||||
/**
|
||||
* Create a pre-coin of the given denomination to be withdrawn from then given
|
||||
@ -369,7 +369,7 @@ export class CryptoImplementation {
|
||||
sig: string,
|
||||
masterPub: string,
|
||||
): boolean {
|
||||
if (versionCurrent === 10) {
|
||||
if (versionCurrent === 10 || versionCurrent === 11) {
|
||||
const paytoHash = hash(stringToBytes(paytoUri + "\0"));
|
||||
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
|
||||
.put(paytoHash)
|
||||
|
@ -15,6 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AmountJson,
|
||||
Amounts,
|
||||
buildCodecForObject,
|
||||
canonicalJson,
|
||||
@ -28,6 +29,7 @@ import {
|
||||
decodeCrock,
|
||||
DenomKeyType,
|
||||
durationFromSpec,
|
||||
GetFeeForDepositRequest,
|
||||
getTimestampNow,
|
||||
Logger,
|
||||
NotificationType,
|
||||
@ -35,6 +37,7 @@ import {
|
||||
TalerErrorDetails,
|
||||
Timestamp,
|
||||
timestampAddDuration,
|
||||
timestampIsBetween,
|
||||
timestampTruncateToSecond,
|
||||
TrackDepositGroupRequest,
|
||||
TrackDepositGroupResponse,
|
||||
@ -49,7 +52,7 @@ import {
|
||||
} from "@gnu-taler/taler-util";
|
||||
import { DepositGroupRecord } from "../db.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 { initRetryInfo, updateRetryInfoTimeout } from "../util/retries.js";
|
||||
import { getExchangeDetails } from "./exchanges.js";
|
||||
@ -58,11 +61,11 @@ import {
|
||||
extractContractData,
|
||||
generateDepositPermissions,
|
||||
getCandidatePayCoins,
|
||||
getEffectiveDepositAmount,
|
||||
getTotalPaymentCost,
|
||||
hashWire,
|
||||
hashWireLegacy,
|
||||
} from "./pay.js";
|
||||
import { getTotalRefreshCost } from "./refresh.js";
|
||||
|
||||
/**
|
||||
* 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(
|
||||
ws: InternalWalletState,
|
||||
req: CreateDepositGroupRequest,
|
||||
@ -495,3 +592,152 @@ export async function createDepositGroup(
|
||||
|
||||
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 {
|
||||
if (coin.suspended) {
|
||||
return false;
|
||||
@ -585,8 +525,7 @@ async function incrementPurchasePayRetry(
|
||||
pr.payRetryInfo.retryCounter++;
|
||||
updateRetryInfoTimeout(pr.payRetryInfo);
|
||||
logger.trace(
|
||||
`retrying pay in ${
|
||||
getDurationRemaining(pr.payRetryInfo.nextRetry).d_ms
|
||||
`retrying pay in ${getDurationRemaining(pr.payRetryInfo.nextRetry).d_ms
|
||||
} ms`,
|
||||
);
|
||||
pr.lastPayError = err;
|
||||
|
@ -83,6 +83,7 @@ export enum WalletApiOperation {
|
||||
AddExchange = "addExchange",
|
||||
GetTransactions = "getTransactions",
|
||||
ListExchanges = "listExchanges",
|
||||
ListKnownBankAccounts = "listKnownBankAccounts",
|
||||
GetWithdrawalDetailsForUri = "getWithdrawalDetailsForUri",
|
||||
GetWithdrawalDetailsForAmount = "getWithdrawalDetailsForAmount",
|
||||
AcceptManualWithdrawal = "acceptManualWithdrawal",
|
||||
@ -279,11 +280,11 @@ export type WalletOperations = {
|
||||
|
||||
export type RequestType<
|
||||
Op extends WalletApiOperation & keyof WalletOperations
|
||||
> = WalletOperations[Op] extends { request: infer T } ? T : never;
|
||||
> = WalletOperations[Op] extends { request: infer T } ? T : never;
|
||||
|
||||
export type ResponseType<
|
||||
Op extends WalletApiOperation & keyof WalletOperations
|
||||
> = WalletOperations[Op] extends { response: infer T } ? T : never;
|
||||
> = WalletOperations[Op] extends { response: infer T } ? T : never;
|
||||
|
||||
export interface WalletCoreApiClient {
|
||||
call<Op extends WalletApiOperation & keyof WalletOperations>(
|
||||
|
@ -41,6 +41,10 @@ import {
|
||||
codecForWithdrawFakebankRequest,
|
||||
URL,
|
||||
parsePaytoUri,
|
||||
KnownBankAccounts,
|
||||
PaytoUri,
|
||||
codecForGetFeeForDeposit,
|
||||
codecForListKnownBankAccounts,
|
||||
} from "@gnu-taler/taler-util";
|
||||
import {
|
||||
addBackupProvider,
|
||||
@ -58,6 +62,7 @@ import { exportBackup } from "./operations/backup/export.js";
|
||||
import { getBalances } from "./operations/balance.js";
|
||||
import {
|
||||
createDepositGroup,
|
||||
getFeeForDeposit,
|
||||
processDepositGroup,
|
||||
trackDepositGroup,
|
||||
} 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(
|
||||
ws: InternalWalletState,
|
||||
): Promise<ExchangesListRespose> {
|
||||
@ -728,6 +757,10 @@ async function dispatchRequestInternal(
|
||||
case "listExchanges": {
|
||||
return await getExchanges(ws);
|
||||
}
|
||||
case "listKnownBankAccounts": {
|
||||
const req = codecForListKnownBankAccounts().decode(payload);
|
||||
return await listKnownBankAccounts(ws, req.currency);
|
||||
}
|
||||
case "getWithdrawalDetailsForUri": {
|
||||
const req = codecForGetWithdrawalDetailsForUri().decode(payload);
|
||||
return await getWithdrawalDetailsForUri(ws, req.talerWithdrawUri);
|
||||
@ -881,6 +914,10 @@ async function dispatchRequestInternal(
|
||||
const resp = await getBackupInfo(ws);
|
||||
return resp;
|
||||
}
|
||||
case "getFeeForDeposit": {
|
||||
const req = codecForGetFeeForDeposit().decode(payload);
|
||||
return await getFeeForDeposit(ws, req);
|
||||
}
|
||||
case "createDepositGroup": {
|
||||
const req = codecForCreateDepositGroupRequest().decode(payload);
|
||||
return await createDepositGroup(ws, req);
|
||||
@ -1004,7 +1041,7 @@ export async function handleCoreApiRequest(
|
||||
try {
|
||||
logger.error("Caught unexpected exception:");
|
||||
logger.error(e.stack);
|
||||
} catch (e) {}
|
||||
} catch (e) { }
|
||||
return {
|
||||
type: "error",
|
||||
operation,
|
||||
|
@ -34,6 +34,7 @@ export enum Pages {
|
||||
welcome = "/welcome",
|
||||
balance = "/balance",
|
||||
manual_withdraw = "/manual-withdraw",
|
||||
deposit = "/deposit/:currency",
|
||||
settings = "/settings",
|
||||
dev = "/dev",
|
||||
cta = "/cta",
|
||||
|
@ -16,9 +16,18 @@
|
||||
|
||||
import { amountFractionalBase, Amounts, Balance } from "@gnu-taler/taler-util";
|
||||
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");
|
||||
return (
|
||||
<TableWithRoundedRows>
|
||||
@ -40,6 +49,11 @@ export function BalanceTable({ balances }: { balances: Balance[] }): VNode {
|
||||
>
|
||||
{v}
|
||||
</td>
|
||||
<td>
|
||||
<ButtonPrimary onClick={() => goToWalletDeposit(av.currency)}>
|
||||
Deposit
|
||||
</ButtonPrimary>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
@ -716,6 +716,10 @@ export const InputWithLabel = styled.div<{ invalid?: boolean }>`
|
||||
}
|
||||
`;
|
||||
|
||||
export const ErrorText = styled.div`
|
||||
color: red;
|
||||
`;
|
||||
|
||||
export const ErrorBox = styled.div`
|
||||
border: 2px solid #f5c6cb;
|
||||
border-radius: 0.25em;
|
||||
|
@ -21,18 +21,21 @@ import { ButtonPrimary, ErrorBox } from "../components/styled/index";
|
||||
import { HookResponse, useAsyncAsHook } from "../hooks/useAsyncAsHook";
|
||||
import { PageLink } from "../renderHtml";
|
||||
import * as wxApi from "../wxApi";
|
||||
|
||||
interface Props {
|
||||
goToWalletDeposit: (currency: string) => void;
|
||||
goToWalletManualWithdraw: () => void;
|
||||
}
|
||||
export function BalancePage({
|
||||
goToWalletManualWithdraw,
|
||||
}: {
|
||||
goToWalletManualWithdraw: () => void;
|
||||
}): VNode {
|
||||
goToWalletDeposit,
|
||||
}: Props): VNode {
|
||||
const state = useAsyncAsHook(wxApi.getBalance);
|
||||
return (
|
||||
<BalanceView
|
||||
balance={state}
|
||||
Linker={PageLink}
|
||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||
goToWalletDeposit={goToWalletDeposit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -40,12 +43,14 @@ export interface BalanceViewProps {
|
||||
balance: HookResponse<BalancesResponse>;
|
||||
Linker: typeof PageLink;
|
||||
goToWalletManualWithdraw: () => void;
|
||||
goToWalletDeposit: (currency: string) => void;
|
||||
}
|
||||
|
||||
export function BalanceView({
|
||||
balance,
|
||||
Linker,
|
||||
goToWalletManualWithdraw,
|
||||
goToWalletDeposit,
|
||||
}: BalanceViewProps): VNode {
|
||||
if (!balance) {
|
||||
return <div>Loading...</div>;
|
||||
@ -71,7 +76,8 @@ export function BalanceView({
|
||||
<Linker pageName="/welcome">help</Linker> getting started?
|
||||
</i18n.Translate>
|
||||
</p>
|
||||
<footer style={{ justifyContent: "space-around" }}>
|
||||
<footer style={{ justifyContent: "space-between" }}>
|
||||
<div />
|
||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||
Withdraw
|
||||
</ButtonPrimary>
|
||||
@ -83,9 +89,13 @@ export function BalanceView({
|
||||
return (
|
||||
<Fragment>
|
||||
<section>
|
||||
<BalanceTable balances={balance.response.balances} />
|
||||
<BalanceTable
|
||||
balances={balance.response.balances}
|
||||
goToWalletDeposit={goToWalletDeposit}
|
||||
/>
|
||||
</section>
|
||||
<footer style={{ justifyContent: "space-around" }}>
|
||||
<footer style={{ justifyContent: "space-between" }}>
|
||||
<div />
|
||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||
Withdraw
|
||||
</ButtonPrimary>
|
||||
|
@ -43,14 +43,17 @@ export function DeveloperPage(): VNode {
|
||||
? []
|
||||
: operationsResponse.response.pendingOperations;
|
||||
|
||||
return <View status={status}
|
||||
return (
|
||||
<View
|
||||
status={status}
|
||||
timedOut={timedOut}
|
||||
operations={operations}
|
||||
onDownloadDatabase={async () => {
|
||||
const db = await wxApi.exportDB()
|
||||
return JSON.stringify(db)
|
||||
const db = await wxApi.exportDB();
|
||||
return JSON.stringify(db);
|
||||
}}
|
||||
/>;
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
@ -64,14 +67,21 @@ function hashObjectId(o: any): string {
|
||||
return JSON.stringify(o);
|
||||
}
|
||||
|
||||
export function View({ status, timedOut, operations, onDownloadDatabase }: Props): VNode {
|
||||
const [downloadedDatabase, setDownloadedDatabase] = useState<{time: Date; content: string}|undefined>(undefined)
|
||||
export function View({
|
||||
status,
|
||||
timedOut,
|
||||
operations,
|
||||
onDownloadDatabase,
|
||||
}: Props): VNode {
|
||||
const [downloadedDatabase, setDownloadedDatabase] = useState<
|
||||
{ time: Date; content: string } | undefined
|
||||
>(undefined);
|
||||
async function onExportDatabase(): Promise<void> {
|
||||
const content = await onDownloadDatabase()
|
||||
const content = await onDownloadDatabase();
|
||||
setDownloadedDatabase({
|
||||
time: new Date(),
|
||||
content
|
||||
})
|
||||
content,
|
||||
});
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
@ -83,9 +93,27 @@ export function View({ status, timedOut, operations, onDownloadDatabase }: Props
|
||||
<button onClick={confirmReset}>reset</button>
|
||||
<br />
|
||||
<button onClick={onExportDatabase}>export database</button>
|
||||
{downloadedDatabase && <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,${btoa(downloadedDatabase.content)}`} download={`taler-wallet-database-${format(downloadedDatabase.time, 'yyyy/MM/dd_HH:mm')}.json`}>click here</a> to download
|
||||
</div>}
|
||||
{downloadedDatabase && (
|
||||
<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 />
|
||||
<Diagnostics diagnostics={status} timedOut={timedOut} />
|
||||
{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 {
|
||||
try {
|
||||
// eslint-disable-next-line no-undef
|
||||
|
@ -84,6 +84,9 @@ function Application() {
|
||||
goToWalletManualWithdraw={() =>
|
||||
goToWalletPage(Pages.manual_withdraw)
|
||||
}
|
||||
goToWalletDeposit={(currency: string) =>
|
||||
goToWalletPage(Pages.deposit.replace(":currency", currency))
|
||||
}
|
||||
/>
|
||||
<Route path={Pages.settings} component={SettingsPage} />
|
||||
<Route
|
||||
@ -107,6 +110,7 @@ function Application() {
|
||||
/>
|
||||
|
||||
<Route path={Pages.history} component={HistoryPage} />
|
||||
|
||||
<Route
|
||||
path={Pages.backup}
|
||||
component={BackupPage}
|
||||
|
@ -24,7 +24,9 @@ import * as wxApi from "../wxApi";
|
||||
|
||||
export function BalancePage({
|
||||
goToWalletManualWithdraw,
|
||||
goToWalletDeposit,
|
||||
}: {
|
||||
goToWalletDeposit: (currency: string) => void;
|
||||
goToWalletManualWithdraw: () => void;
|
||||
}): VNode {
|
||||
const state = useAsyncAsHook(wxApi.getBalance);
|
||||
@ -33,6 +35,7 @@ export function BalancePage({
|
||||
balance={state}
|
||||
Linker={PageLink}
|
||||
goToWalletManualWithdraw={goToWalletManualWithdraw}
|
||||
goToWalletDeposit={goToWalletDeposit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -41,12 +44,14 @@ export interface BalanceViewProps {
|
||||
balance: HookResponse<BalancesResponse>;
|
||||
Linker: typeof PageLink;
|
||||
goToWalletManualWithdraw: () => void;
|
||||
goToWalletDeposit: (currency: string) => void;
|
||||
}
|
||||
|
||||
export function BalanceView({
|
||||
balance,
|
||||
Linker,
|
||||
goToWalletManualWithdraw,
|
||||
goToWalletDeposit,
|
||||
}: BalanceViewProps): VNode {
|
||||
if (!balance) {
|
||||
return <div>Loading...</div>;
|
||||
@ -65,28 +70,35 @@ export function BalanceView({
|
||||
}
|
||||
if (balance.response.balances.length === 0) {
|
||||
return (
|
||||
<Fragment>
|
||||
<p>
|
||||
<Centered style={{ marginTop: 100 }}>
|
||||
<i18n.Translate>
|
||||
You have no balance to show. Need some{" "}
|
||||
<Linker pageName="/welcome">help</Linker> getting started?
|
||||
</i18n.Translate>
|
||||
<div>
|
||||
</Centered>
|
||||
</p>
|
||||
<footer style={{ justifyContent: "space-between" }}>
|
||||
<div />
|
||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||
Withdraw
|
||||
</ButtonPrimary>
|
||||
</div>
|
||||
</Centered>
|
||||
</p>
|
||||
</footer>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<section>
|
||||
<BalanceTable balances={balance.response.balances} />
|
||||
<BalanceTable
|
||||
balances={balance.response.balances}
|
||||
goToWalletDeposit={goToWalletDeposit}
|
||||
/>
|
||||
</section>
|
||||
<footer style={{ justifyContent: "space-around" }}>
|
||||
<footer style={{ justifyContent: "space-between" }}>
|
||||
<div />
|
||||
<ButtonPrimary onClick={goToWalletManualWithdraw}>
|
||||
Withdraw
|
||||
</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) {
|
||||
const fee = Amounts.sub(
|
||||
Amounts.parseOrThrow(transaction.amountRaw),
|
||||
Amounts.parseOrThrow(transaction.amountEffective),
|
||||
Amounts.parseOrThrow(transaction.amountRaw),
|
||||
).amount;
|
||||
return (
|
||||
<TransactionTemplate>
|
||||
@ -379,15 +379,15 @@ export function TransactionView({
|
||||
<br />
|
||||
<Part
|
||||
big
|
||||
title="Total deposit"
|
||||
title="Total send"
|
||||
text={amountToString(transaction.amountEffective)}
|
||||
kind="negative"
|
||||
kind="neutral"
|
||||
/>
|
||||
<Part
|
||||
big
|
||||
title="Purchase amount"
|
||||
title="Deposit amount"
|
||||
text={amountToString(transaction.amountRaw)}
|
||||
kind="neutral"
|
||||
kind="positive"
|
||||
/>
|
||||
<Part big title="Fee" text={amountToString(fee)} kind="negative" />
|
||||
</TransactionTemplate>
|
||||
|
@ -45,6 +45,7 @@ import { WalletBox } from "./components/styled";
|
||||
import { ProviderDetailPage } from "./wallet/ProviderDetailPage";
|
||||
import { ProviderAddPage } from "./wallet/ProviderAddPage";
|
||||
import { ExchangeAddPage } from "./wallet/ExchangeAddPage";
|
||||
import { DepositPage } from "./wallet/DepositPage";
|
||||
|
||||
function main(): void {
|
||||
try {
|
||||
@ -105,6 +106,9 @@ function Application(): VNode {
|
||||
path={Pages.balance}
|
||||
component={withLogoAndNavBar(BalancePage)}
|
||||
goToWalletManualWithdraw={() => route(Pages.manual_withdraw)}
|
||||
goToWalletDeposit={(currency: string) =>
|
||||
route(Pages.deposit.replace(":currency", currency))
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path={Pages.settings}
|
||||
@ -145,6 +149,10 @@ function Application(): VNode {
|
||||
component={withLogoAndNavBar(ManualWithdrawPage)}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={Pages.deposit}
|
||||
component={withLogoAndNavBar(DepositPage)}
|
||||
/>
|
||||
<Route
|
||||
path={Pages.reset_required}
|
||||
component={() => <div>no yet implemented</div>}
|
||||
|
@ -24,10 +24,11 @@
|
||||
import {
|
||||
AcceptExchangeTosRequest,
|
||||
AcceptManualWithdrawalResult, AcceptTipRequest, AcceptWithdrawalResponse,
|
||||
AddExchangeRequest, ApplyRefundResponse, BalancesResponse, ConfirmPayResult,
|
||||
CoreApiResponse, DeleteTransactionRequest, ExchangesListRespose,
|
||||
AddExchangeRequest, AmountJson, AmountString, ApplyRefundResponse, BalancesResponse, ConfirmPayResult,
|
||||
CoreApiResponse, CreateDepositGroupRequest, CreateDepositGroupResponse, DeleteTransactionRequest, ExchangesListRespose,
|
||||
GetExchangeTosResult, GetExchangeWithdrawalInfo,
|
||||
GetWithdrawalDetailsForUriRequest, NotificationType, PreparePayResult, PrepareTipRequest,
|
||||
GetFeeForDepositRequest,
|
||||
GetWithdrawalDetailsForUriRequest, KnownBankAccounts, NotificationType, PreparePayResult, PrepareTipRequest,
|
||||
PrepareTipResult, RetryTransactionRequest,
|
||||
SetWalletDeviceIdRequest, TransactionsResponse, WalletDiagnostics, WithdrawUriInfoResponse
|
||||
} from "@gnu-taler/taler-util";
|
||||
@ -36,6 +37,7 @@ import {
|
||||
PendingOperationsResponse,
|
||||
RemoveBackupProviderRequest
|
||||
} 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 { MessageFromBackend } from "./wxBackend.js";
|
||||
|
||||
@ -119,6 +121,18 @@ export function resetDb(): Promise<void> {
|
||||
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.
|
||||
*/
|
||||
@ -170,6 +184,9 @@ export function listKnownCurrencies(): Promise<ListOfKnownCurrencies> {
|
||||
export function listExchanges(): Promise<ExchangesListRespose> {
|
||||
return callBackend("listExchanges", {});
|
||||
}
|
||||
export function listKnownBankAccounts(currency?: string): Promise<KnownBankAccounts> {
|
||||
return callBackend("listKnownBankAccounts", { currency });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current state of wallet backups.
|
||||
|
Loading…
Reference in New Issue
Block a user