diff --git a/packages/taler-util/src/walletTypes.ts b/packages/taler-util/src/walletTypes.ts
index ced30e4db..4158dde9e 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -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 =>
export interface GetWithdrawalDetailsForUriRequest {
talerWithdrawUri: string;
}
-
export const codecForGetWithdrawalDetailsForUri = (): Codec =>
buildCodecForObject()
.property("talerWithdrawUri", codecForString())
.build("GetWithdrawalDetailsForUriRequest");
+export interface ListKnownBankAccountsRequest {
+ currency?: string;
+}
+export const codecForListKnownBankAccounts = (): Codec =>
+ buildCodecForObject()
+ .property("currency", codecOptional(codecForString()))
+ .build("ListKnownBankAccountsRequest");
+
export interface GetExchangeWithdrawalInfo {
exchangeBaseUrl: string;
amount: AmountJson;
@@ -965,11 +977,23 @@ export const codecForAbortPayWithRefundRequest = (): Codec =>
+ buildCodecForObject()
+ .property("amount", codecForAmountString())
+ .property("depositPaytoUri", codecForString())
+ .build("GetFeeForDepositRequest");
+
export const codecForCreateDepositGroupRequest = (): Codec =>
buildCodecForObject()
.property("amount", codecForAmountString())
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
index 04bc2d9bc..b5987582a 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -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)
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts b/packages/taler-wallet-core/src/operations/deposits.ts
index f90172a45..6d28c23e5 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -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 {
+ 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 {
+ const amt: AmountJson[] = [];
+ const fees: AmountJson[] = [];
+ const exchangeSet: Set = 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 {
+ const wireFee: AmountJson[] = [];
+ const coinFee: AmountJson[] = [];
+ const refreshFee: AmountJson[] = [];
+ const exchangeSet: Set = 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
+ };
+}
diff --git a/packages/taler-wallet-core/src/operations/pay.ts b/packages/taler-wallet-core/src/operations/pay.ts
index 63ccc6531..89930120d 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -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 {
- const amt: AmountJson[] = [];
- const fees: AmountJson[] = [];
- const exchangeSet: Set = 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;
diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts
index 445c0539a..0555b0ced 100644
--- a/packages/taler-wallet-core/src/wallet-api-types.ts
+++ b/packages/taler-wallet-core/src/wallet-api-types.ts
@@ -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(
diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts
index ed0046c59..2f94d5e82 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -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 {
+ 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 {
@@ -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,
diff --git a/packages/taler-wallet-webextension/src/NavigationBar.tsx b/packages/taler-wallet-webextension/src/NavigationBar.tsx
index 8dc73efdb..e7108679c 100644
--- a/packages/taler-wallet-webextension/src/NavigationBar.tsx
+++ b/packages/taler-wallet-webextension/src/NavigationBar.tsx
@@ -34,6 +34,7 @@ export enum Pages {
welcome = "/welcome",
balance = "/balance",
manual_withdraw = "/manual-withdraw",
+ deposit = "/deposit/:currency",
settings = "/settings",
dev = "/dev",
cta = "/cta",
diff --git a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
index e1c19cc23..cf396e129 100644
--- a/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
+++ b/packages/taler-wallet-webextension/src/components/BalanceTable.tsx
@@ -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 (
@@ -40,6 +49,11 @@ export function BalanceTable({ balances }: { balances: Balance[] }): VNode {
>
{v}
+
+ goToWalletDeposit(av.currency)}>
+ Deposit
+
+ |
);
})}
diff --git a/packages/taler-wallet-webextension/src/components/styled/index.tsx b/packages/taler-wallet-webextension/src/components/styled/index.tsx
index a5c9f2837..216a1fabc 100644
--- a/packages/taler-wallet-webextension/src/components/styled/index.tsx
+++ b/packages/taler-wallet-webextension/src/components/styled/index.tsx
@@ -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;
diff --git a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
index 33164783d..40499b87c 100644
--- a/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
+++ b/packages/taler-wallet-webextension/src/popup/BalancePage.tsx
@@ -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 (
);
}
@@ -40,12 +43,14 @@ export interface BalanceViewProps {
balance: HookResponse;
Linker: typeof PageLink;
goToWalletManualWithdraw: () => void;
+ goToWalletDeposit: (currency: string) => void;
}
export function BalanceView({
balance,
Linker,
goToWalletManualWithdraw,
+ goToWalletDeposit,
}: BalanceViewProps): VNode {
if (!balance) {
return Loading...
;
@@ -71,7 +76,8 @@ export function BalanceView({
help getting started?
-