wallet-core: move contract terms to object store

This commit is contained in:
Florian Dold 2023-09-12 13:48:52 +02:00
parent ee8993f11c
commit 4b0680eefa
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
5 changed files with 142 additions and 97 deletions

View File

@ -28,6 +28,7 @@ import {
} from "@gnu-taler/idb-bridge"; } from "@gnu-taler/idb-bridge";
import { import {
AgeCommitmentProof, AgeCommitmentProof,
AmountJson,
AmountString, AmountString,
Amounts, Amounts,
AttentionInfo, AttentionInfo,
@ -1640,6 +1641,15 @@ export interface DepositTrackingInfo {
export interface DepositGroupRecord { export interface DepositGroupRecord {
depositGroupId: string; depositGroupId: string;
currency: string;
/**
* Instructed amount.
*/
amount: AmountString;
wireTransferDeadline: TalerProtocolTimestamp;
merchantPub: string; merchantPub: string;
merchantPriv: string; merchantPriv: string;
@ -1655,13 +1665,6 @@ export interface DepositGroupRecord {
salt: string; salt: string;
}; };
/**
* Verbatim contract terms.
*
* FIXME: Move this to the contract terms object store!
*/
contractTermsRaw: MerchantContractTerms;
contractTermsHash: string; contractTermsHash: string;
payCoinSelection: PayCoinSelection; payCoinSelection: PayCoinSelection;
@ -1981,7 +1984,9 @@ export interface PeerPullPaymentIncomingRecord {
exchangeBaseUrl: string; exchangeBaseUrl: string;
contractTerms: PeerContractTerms; amount: AmountString;
contractTermsHash: string;
timestampCreated: TalerPreciseTimestamp; timestampCreated: TalerPreciseTimestamp;

View File

@ -33,12 +33,13 @@ import {
AmountString, AmountString,
codecForAny, codecForAny,
codecForBankWithdrawalOperationPostResponse, codecForBankWithdrawalOperationPostResponse,
codecForDepositSuccess, codecForBatchDepositSuccess,
codecForExchangeMeltResponse, codecForExchangeMeltResponse,
codecForExchangeRevealResponse, codecForExchangeRevealResponse,
codecForWithdrawResponse, codecForWithdrawResponse,
DenominationPubKey, DenominationPubKey,
encodeCrock, encodeCrock,
ExchangeBatchDepositRequest,
ExchangeMeltRequest, ExchangeMeltRequest,
ExchangeProtocolVersion, ExchangeProtocolVersion,
ExchangeWithdrawRequest, ExchangeWithdrawRequest,
@ -256,22 +257,27 @@ export async function depositCoin(args: {
refundDeadline: refundDeadline, refundDeadline: refundDeadline,
wireInfoHash: hashWire(depositPayto, wireSalt), wireInfoHash: hashWire(depositPayto, wireSalt),
}); });
const requestBody = { const requestBody: ExchangeBatchDepositRequest = {
contribution: Amounts.stringify(dp.contribution), coins: [
{
contribution: Amounts.stringify(dp.contribution),
coin_pub: dp.coin_pub,
coin_sig: dp.coin_sig,
denom_pub_hash: dp.h_denom,
ub_sig: dp.ub_sig,
},
],
merchant_payto_uri: depositPayto, merchant_payto_uri: depositPayto,
wire_salt: wireSalt, wire_salt: wireSalt,
h_contract_terms: contractTermsHash, h_contract_terms: contractTermsHash,
ub_sig: coin.denomSig,
timestamp: depositTimestamp, timestamp: depositTimestamp,
wire_transfer_deadline: wireTransferDeadline, wire_transfer_deadline: wireTransferDeadline,
refund_deadline: refundDeadline, refund_deadline: refundDeadline,
coin_sig: dp.coin_sig,
denom_pub_hash: dp.h_denom,
merchant_pub: merchantPub, merchant_pub: merchantPub,
}; };
const url = new URL(`coins/${dp.coin_pub}/deposit`, dp.exchange_url); const url = new URL(`batch-deposit`, dp.exchange_url);
const httpResp = await http.postJson(url.href, requestBody); const httpResp = await http.fetch(url.href, { body: requestBody });
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); await readSuccessResponseJsonOrThrow(httpResp, codecForBatchDepositSuccess());
} }
export async function refreshCoin(req: { export async function refreshCoin(req: {

View File

@ -884,8 +884,18 @@ async function processDepositGroupPendingDeposit(
): Promise<TaskRunResult> { ): Promise<TaskRunResult> {
logger.info("processing deposit group in pending(deposit)"); logger.info("processing deposit group in pending(deposit)");
const depositGroupId = depositGroup.depositGroupId; const depositGroupId = depositGroup.depositGroupId;
const contractTermsRec = await ws.db
.mktx((x) => [x.contractTerms])
.runReadOnly(async (tx) => {
return tx.contractTerms.get(depositGroup.contractTermsHash);
});
if (!contractTermsRec) {
throw Error("contract terms for deposit not found in database");
}
const contractTerms: MerchantContractTerms =
contractTermsRec.contractTermsRaw;
const contractData = extractContractData( const contractData = extractContractData(
depositGroup.contractTermsRaw, contractTermsRec.contractTermsRaw,
depositGroup.contractTermsHash, depositGroup.contractTermsHash,
"", "",
); );
@ -921,12 +931,11 @@ async function processDepositGroupPendingDeposit(
coins, coins,
h_contract_terms: depositGroup.contractTermsHash, h_contract_terms: depositGroup.contractTermsHash,
merchant_payto_uri: depositGroup.wire.payto_uri, merchant_payto_uri: depositGroup.wire.payto_uri,
merchant_pub: depositGroup.contractTermsRaw.merchant_pub, merchant_pub: contractTerms.merchant_pub,
timestamp: depositGroup.contractTermsRaw.timestamp, timestamp: contractTerms.timestamp,
wire_salt: depositGroup.wire.salt, wire_salt: depositGroup.wire.salt,
wire_transfer_deadline: wire_transfer_deadline: contractTerms.wire_transfer_deadline,
depositGroup.contractTermsRaw.wire_transfer_deadline, refund_deadline: contractTerms.refund_deadline,
refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
}; };
for (let i = 0; i < depositPermissions.length; i++) { for (let i = 0; i < depositPermissions.length; i++) {
@ -1086,7 +1095,10 @@ async function trackDeposit(
coinPub: string, coinPub: string,
exchangeUrl: string, exchangeUrl: string,
): Promise<TrackTransaction> { ): Promise<TrackTransaction> {
const wireHash = depositGroup.contractTermsRaw.h_wire; const wireHash = hashWire(
depositGroup.wire.payto_uri,
depositGroup.wire.salt,
);
const url = new URL( const url = new URL(
`deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${coinPub}`, `deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${coinPub}`,
@ -1358,8 +1370,9 @@ export async function createDepositGroup(
const depositGroup: DepositGroupRecord = { const depositGroup: DepositGroupRecord = {
contractTermsHash, contractTermsHash,
contractTermsRaw: contractTerms,
depositGroupId, depositGroupId,
currency: Amounts.currencyOf(totalDepositCost),
amount: contractData.amount,
noncePriv: noncePair.priv, noncePriv: noncePair.priv,
noncePub: noncePair.pub, noncePub: noncePair.pub,
timestampCreated: AbsoluteTime.toPreciseTimestamp(now), timestampCreated: AbsoluteTime.toPreciseTimestamp(now),
@ -1375,6 +1388,7 @@ export async function createDepositGroup(
counterpartyEffectiveDepositAmount: Amounts.stringify( counterpartyEffectiveDepositAmount: Amounts.stringify(
counterpartyEffectiveDepositAmount, counterpartyEffectiveDepositAmount,
), ),
wireTransferDeadline: contractTerms.wire_transfer_deadline,
wire: { wire: {
payto_uri: req.depositPaytoUri, payto_uri: req.depositPaytoUri,
salt: wireSalt, salt: wireSalt,
@ -1395,6 +1409,7 @@ export async function createDepositGroup(
x.denominations, x.denominations,
x.refreshGroups, x.refreshGroups,
x.coinAvailability, x.coinAvailability,
x.contractTerms,
]) ])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
await spendCoins(ws, tx, { await spendCoins(ws, tx, {
@ -1406,6 +1421,10 @@ export async function createDepositGroup(
refreshReason: RefreshReason.PayDeposit, refreshReason: RefreshReason.PayDeposit,
}); });
await tx.depositGroups.put(depositGroup); await tx.depositGroups.put(depositGroup);
await tx.contractTerms.put({
contractTermsRaw: contractTerms,
h: contractTermsHash,
});
return computeDepositTransactionStatus(depositGroup); return computeDepositTransactionStatus(depositGroup);
}); });

View File

@ -19,6 +19,7 @@ import {
Amounts, Amounts,
CoinRefreshRequest, CoinRefreshRequest,
ConfirmPeerPullDebitRequest, ConfirmPeerPullDebitRequest,
ContractTermsUtil,
ExchangePurseDeposits, ExchangePurseDeposits,
HttpStatusCode, HttpStatusCode,
Logger, Logger,
@ -103,9 +104,7 @@ async function handlePurseCreationConflict(
throw new TalerProtocolViolationError(); throw new TalerProtocolViolationError();
} }
const instructedAmount = Amounts.parseOrThrow( const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
peerPullInc.contractTerms.amount,
);
const sel = peerPullInc.coinSel; const sel = peerPullInc.coinSel;
if (!sel) { if (!sel) {
@ -142,9 +141,7 @@ async function handlePurseCreationConflict(
await ws.db await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const myPpi = await tx.peerPullDebit.get( const myPpi = await tx.peerPullDebit.get(peerPullInc.peerPullDebitId);
peerPullInc.peerPullDebitId,
);
if (!myPpi) { if (!myPpi) {
return; return;
} }
@ -220,9 +217,7 @@ async function processPeerPullDebitPendingDeposit(
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pi = await tx.peerPullDebit.get( const pi = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!pi) { if (!pi) {
throw Error("peer pull payment not found anymore"); throw Error("peer pull payment not found anymore");
} }
@ -248,9 +243,7 @@ async function processPeerPullDebitPendingDeposit(
x.coins, x.coins,
]) ])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pi = await tx.peerPullDebit.get( const pi = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!pi) { if (!pi) {
throw Error("peer pull payment not found anymore"); throw Error("peer pull payment not found anymore");
} }
@ -335,9 +328,7 @@ async function processPeerPullDebitAbortingRefresh(
} }
} }
if (newOpState) { if (newOpState) {
const newDg = await tx.peerPullDebit.get( const newDg = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!newDg) { if (!newDg) {
return; return;
} }
@ -391,9 +382,7 @@ export async function confirmPeerPullDebit(
} else if (req.peerPullDebitId) { } else if (req.peerPullDebitId) {
peerPullDebitId = req.peerPullDebitId; peerPullDebitId = req.peerPullDebitId;
} else { } else {
throw Error( throw Error("invalid request, transactionId or peerPullDebitId required");
"invalid request, transactionId or peerPullDebitId required",
);
} }
const peerPullInc = await ws.db const peerPullInc = await ws.db
@ -408,9 +397,7 @@ export async function confirmPeerPullDebit(
); );
} }
const instructedAmount = Amounts.parseOrThrow( const instructedAmount = Amounts.parseOrThrow(peerPullInc.amount);
peerPullInc.contractTerms.amount,
);
const coinSelRes = await selectPeerCoins(ws, { instructedAmount }); const coinSelRes = await selectPeerCoins(ws, { instructedAmount });
logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`); logger.info(`selected p2p coins (pull): ${j2s(coinSelRes)}`);
@ -454,9 +441,7 @@ export async function confirmPeerPullDebit(
refreshReason: RefreshReason.PayPeerPull, refreshReason: RefreshReason.PayPeerPull,
}); });
const pi = await tx.peerPullDebit.get( const pi = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!pi) { if (!pi) {
throw Error(); throw Error();
} }
@ -498,27 +483,36 @@ export async function preparePeerPullDebit(
throw Error("got invalid taler://pay-pull URI"); throw Error("got invalid taler://pay-pull URI");
} }
const existingPullIncomingRecord = await ws.db const existing = await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit, x.contractTerms])
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
return tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([ const peerPullDebitRecord =
uri.exchangeBaseUrl, await tx.peerPullDebit.indexes.byExchangeAndContractPriv.get([
uri.contractPriv, uri.exchangeBaseUrl,
]); uri.contractPriv,
]);
if (!peerPullDebitRecord) {
return;
}
const contractTerms = await tx.contractTerms.get(
peerPullDebitRecord.contractTermsHash,
);
if (!contractTerms) {
return;
}
return { peerPullDebitRecord, contractTerms };
}); });
if (existingPullIncomingRecord) { if (existing) {
return { return {
amount: existingPullIncomingRecord.contractTerms.amount, amount: existing.peerPullDebitRecord.amount,
amountRaw: existingPullIncomingRecord.contractTerms.amount, amountRaw: existing.peerPullDebitRecord.amount,
amountEffective: existingPullIncomingRecord.totalCostEstimated, amountEffective: existing.peerPullDebitRecord.totalCostEstimated,
contractTerms: existingPullIncomingRecord.contractTerms, contractTerms: existing.contractTerms.contractTermsRaw,
peerPullDebitId: peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
existingPullIncomingRecord.peerPullDebitId,
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
tag: TransactionType.PeerPullDebit, tag: TransactionType.PeerPullDebit,
peerPullDebitId: peerPullDebitId: existing.peerPullDebitRecord.peerPullDebitId,
existingPullIncomingRecord.peerPullDebitId,
}), }),
}; };
} }
@ -566,6 +560,8 @@ export async function preparePeerPullDebit(
throw Error("pull payments without contract terms not supported yet"); throw Error("pull payments without contract terms not supported yet");
} }
const contractTermsHash = ContractTermsUtil.hashContractTerms(contractTerms);
// FIXME: Why don't we compute the totalCost here?! // FIXME: Why don't we compute the totalCost here?!
const instructedAmount = Amounts.parseOrThrow(contractTerms.amount); const instructedAmount = Amounts.parseOrThrow(contractTerms.amount);
@ -588,18 +584,23 @@ export async function preparePeerPullDebit(
); );
await ws.db await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit, x.contractTerms])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
await tx.peerPullDebit.add({ await tx.contractTerms.put({
peerPullDebitId, h: contractTermsHash,
contractPriv: contractPriv, contractTermsRaw: contractTerms,
exchangeBaseUrl: exchangeBaseUrl, }),
pursePub: pursePub, await tx.peerPullDebit.add({
timestampCreated: TalerPreciseTimestamp.now(), peerPullDebitId,
contractTerms, contractPriv: contractPriv,
status: PeerPullDebitRecordStatus.DialogProposed, exchangeBaseUrl: exchangeBaseUrl,
totalCostEstimated: Amounts.stringify(totalAmount), pursePub: pursePub,
}); timestampCreated: TalerPreciseTimestamp.now(),
contractTermsHash,
amount: contractTerms.amount,
status: PeerPullDebitRecordStatus.DialogProposed,
totalCostEstimated: Amounts.stringify(totalAmount),
});
}); });
return { return {
@ -631,9 +632,7 @@ export async function suspendPeerPullDebitTransaction(
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get( const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!pullDebitRec) { if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`); logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return; return;
@ -692,9 +691,7 @@ export async function abortPeerPullDebitTransaction(
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get( const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!pullDebitRec) { if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`); logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return; return;
@ -753,9 +750,7 @@ export async function failPeerPullDebitTransaction(
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get( const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!pullDebitRec) { if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`); logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return; return;
@ -814,9 +809,7 @@ export async function resumePeerPullDebitTransaction(
const transitionInfo = await ws.db const transitionInfo = await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const pullDebitRec = await tx.peerPullDebit.get( const pullDebitRec = await tx.peerPullDebit.get(peerPullDebitId);
peerPullDebitId,
);
if (!pullDebitRec) { if (!pullDebitRec) {
logger.warn(`peer pull debit ${peerPullDebitId} not found`); logger.warn(`peer pull debit ${peerPullDebitId} not found`);
return; return;

View File

@ -346,11 +346,19 @@ export async function getTransactionById(
} }
case TransactionType.PeerPullDebit: { case TransactionType.PeerPullDebit: {
return await ws.db return await ws.db
.mktx((x) => [x.peerPullDebit]) .mktx((x) => [x.peerPullDebit, x.contractTerms])
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {
const debit = await tx.peerPullDebit.get(parsedTx.peerPullDebitId); const debit = await tx.peerPullDebit.get(parsedTx.peerPullDebitId);
if (!debit) throw Error("not found"); if (!debit) throw Error("not found");
return buildTransactionForPullPaymentDebit(debit); const contractTermsRec = await tx.contractTerms.get(
debit.contractTermsHash,
);
if (!contractTermsRec)
throw Error("contract terms for peer-pull-debit not found");
return buildTransactionForPullPaymentDebit(
debit,
contractTermsRec.contractTermsRaw,
);
}); });
} }
@ -477,6 +485,7 @@ function buildTransactionForPushPaymentDebit(
function buildTransactionForPullPaymentDebit( function buildTransactionForPullPaymentDebit(
pi: PeerPullPaymentIncomingRecord, pi: PeerPullPaymentIncomingRecord,
contractTerms: PeerContractTerms,
ort?: OperationRetryRecord, ort?: OperationRetryRecord,
): Transaction { ): Transaction {
return { return {
@ -485,12 +494,12 @@ function buildTransactionForPullPaymentDebit(
txActions: computePeerPullDebitTransactionActions(pi), txActions: computePeerPullDebitTransactionActions(pi),
amountEffective: pi.coinSel?.totalCost amountEffective: pi.coinSel?.totalCost
? pi.coinSel?.totalCost ? pi.coinSel?.totalCost
: Amounts.stringify(pi.contractTerms.amount), : Amounts.stringify(pi.amount),
amountRaw: Amounts.stringify(pi.contractTerms.amount), amountRaw: Amounts.stringify(pi.amount),
exchangeBaseUrl: pi.exchangeBaseUrl, exchangeBaseUrl: pi.exchangeBaseUrl,
info: { info: {
expiration: pi.contractTerms.purse_expiration, expiration: contractTerms.purse_expiration,
summary: pi.contractTerms.summary, summary: contractTerms.summary,
}, },
timestamp: pi.timestampCreated, timestamp: pi.timestampCreated,
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
@ -805,7 +814,7 @@ function buildTransactionForDeposit(
amountEffective: Amounts.stringify(dg.totalPayCost), amountEffective: Amounts.stringify(dg.totalPayCost),
timestamp: dg.timestampCreated, timestamp: dg.timestampCreated,
targetPaytoUri: dg.wire.payto_uri, targetPaytoUri: dg.wire.payto_uri,
wireTransferDeadline: dg.contractTermsRaw.wire_transfer_deadline, wireTransferDeadline: dg.wireTransferDeadline,
transactionId: constructTransactionIdentifier({ transactionId: constructTransactionIdentifier({
tag: TransactionType.Deposit, tag: TransactionType.Deposit,
depositGroupId: dg.depositGroupId, depositGroupId: dg.depositGroupId,
@ -980,7 +989,7 @@ export async function getTransactions(
}); });
await iterRecordsForPeerPullDebit(tx, filter, async (pi) => { await iterRecordsForPeerPullDebit(tx, filter, async (pi) => {
const amount = Amounts.parseOrThrow(pi.contractTerms.amount); const amount = Amounts.parseOrThrow(pi.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) { if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return; return;
} }
@ -991,10 +1000,23 @@ export async function getTransactions(
pi.status !== PeerPullDebitRecordStatus.PendingDeposit && pi.status !== PeerPullDebitRecordStatus.PendingDeposit &&
pi.status !== PeerPullDebitRecordStatus.Done pi.status !== PeerPullDebitRecordStatus.Done
) { ) {
// FIXME: Why?!
return; return;
} }
transactions.push(buildTransactionForPullPaymentDebit(pi)); const contractTermsRec = await tx.contractTerms.get(
pi.contractTermsHash,
);
if (!contractTermsRec) {
return;
}
transactions.push(
buildTransactionForPullPaymentDebit(
pi,
contractTermsRec.contractTermsRaw,
),
);
}); });
await iterRecordsForPeerPushCredit(tx, filter, async (pi) => { await iterRecordsForPeerPushCredit(tx, filter, async (pi) => {
@ -1158,7 +1180,7 @@ export async function getTransactions(
}); });
await iterRecordsForDeposit(tx, filter, async (dg) => { await iterRecordsForDeposit(tx, filter, async (dg) => {
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount); const amount = Amounts.parseOrThrow(dg.amount);
if (shouldSkipCurrency(transactionsRequest, amount.currency)) { if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
return; return;
} }