implement deposits
This commit is contained in:
parent
f884193b1a
commit
5f3c02d31a
@ -503,6 +503,37 @@ backupCli
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const depositCli = walletCli.subcommand("depositArgs", "deposit", {
|
||||||
|
help: "Subcommands for depositing money to payto:// accounts",
|
||||||
|
});
|
||||||
|
|
||||||
|
depositCli
|
||||||
|
.subcommand("createDepositArgs", "create")
|
||||||
|
.requiredArgument("amount", clk.STRING)
|
||||||
|
.requiredArgument("targetPayto", clk.STRING)
|
||||||
|
.action(async (args) => {
|
||||||
|
await withWallet(args, async (wallet) => {
|
||||||
|
const resp = await wallet.createDepositGroup({
|
||||||
|
amount: args.createDepositArgs.amount,
|
||||||
|
depositPaytoUri: args.createDepositArgs.targetPayto,
|
||||||
|
});
|
||||||
|
console.log(`Created deposit ${resp.depositGroupId}`);
|
||||||
|
await wallet.runPending();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
depositCli
|
||||||
|
.subcommand("trackDepositArgs", "track")
|
||||||
|
.requiredArgument("depositGroupId", clk.STRING)
|
||||||
|
.action(async (args) => {
|
||||||
|
await withWallet(args, async (wallet) => {
|
||||||
|
const resp = await wallet.trackDepositGroup({
|
||||||
|
depositGroupId: args.trackDepositArgs.depositGroupId,
|
||||||
|
});
|
||||||
|
console.log(JSON.stringify(resp, undefined, 2));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
|
const advancedCli = walletCli.subcommand("advancedArgs", "advanced", {
|
||||||
help:
|
help:
|
||||||
"Subcommands for advanced operations (only use if you know what you're doing!).",
|
"Subcommands for advanced operations (only use if you know what you're doing!).",
|
||||||
|
@ -78,6 +78,10 @@ import {
|
|||||||
AbortPayWithRefundRequest,
|
AbortPayWithRefundRequest,
|
||||||
openPromise,
|
openPromise,
|
||||||
parsePaytoUri,
|
parsePaytoUri,
|
||||||
|
CreateDepositGroupRequest,
|
||||||
|
CreateDepositGroupResponse,
|
||||||
|
TrackDepositGroupRequest,
|
||||||
|
TrackDepositGroupResponse,
|
||||||
} from "taler-wallet-core";
|
} from "taler-wallet-core";
|
||||||
import { URL } from "url";
|
import { URL } from "url";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
@ -873,6 +877,9 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
|
|
||||||
config.setString("exchangedb-postgres", "config", e.database);
|
config.setString("exchangedb-postgres", "config", e.database);
|
||||||
|
|
||||||
|
config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s");
|
||||||
|
config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s");
|
||||||
|
|
||||||
const exchangeMasterKey = createEddsaKeyPair();
|
const exchangeMasterKey = createEddsaKeyPair();
|
||||||
|
|
||||||
config.setString(
|
config.setString(
|
||||||
@ -1017,13 +1024,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
this.globalState,
|
this.globalState,
|
||||||
"exchange-offline",
|
"exchange-offline",
|
||||||
"taler-exchange-offline",
|
"taler-exchange-offline",
|
||||||
[
|
["-c", this.configFilename, "download", "sign", "upload"],
|
||||||
"-c",
|
|
||||||
this.configFilename,
|
|
||||||
"download",
|
|
||||||
"sign",
|
|
||||||
"upload",
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const accounts: string[] = [];
|
const accounts: string[] = [];
|
||||||
@ -1049,13 +1050,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
this.globalState,
|
this.globalState,
|
||||||
"exchange-offline",
|
"exchange-offline",
|
||||||
"taler-exchange-offline",
|
"taler-exchange-offline",
|
||||||
[
|
["-c", this.configFilename, "enable-account", acc, "upload"],
|
||||||
"-c",
|
|
||||||
this.configFilename,
|
|
||||||
"enable-account",
|
|
||||||
acc,
|
|
||||||
"upload",
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1615,6 +1610,16 @@ export class WalletCli {
|
|||||||
throw new OperationFailedError(resp.error);
|
throw new OperationFailedError(resp.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createDepositGroup(
|
||||||
|
req: CreateDepositGroupRequest,
|
||||||
|
): Promise<CreateDepositGroupResponse> {
|
||||||
|
const resp = await this.apiRequest("createDepositGroup", req);
|
||||||
|
if (resp.type === "response") {
|
||||||
|
return resp.result as CreateDepositGroupResponse;
|
||||||
|
}
|
||||||
|
throw new OperationFailedError(resp.error);
|
||||||
|
}
|
||||||
|
|
||||||
async abortFailedPayWithRefund(
|
async abortFailedPayWithRefund(
|
||||||
req: AbortPayWithRefundRequest,
|
req: AbortPayWithRefundRequest,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -1714,6 +1719,16 @@ export class WalletCli {
|
|||||||
throw new OperationFailedError(resp.error);
|
throw new OperationFailedError(resp.error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async trackDepositGroup(
|
||||||
|
req: TrackDepositGroupRequest,
|
||||||
|
): Promise<TrackDepositGroupResponse> {
|
||||||
|
const resp = await this.apiRequest("trackDepositGroup", req);
|
||||||
|
if (resp.type === "response") {
|
||||||
|
return resp.result as TrackDepositGroupResponse;
|
||||||
|
}
|
||||||
|
throw new OperationFailedError(resp.error);
|
||||||
|
}
|
||||||
|
|
||||||
async runIntegrationTest(args: IntegrationTestArgs): Promise<void> {
|
async runIntegrationTest(args: IntegrationTestArgs): Promise<void> {
|
||||||
const resp = await this.apiRequest("runIntegrationTest", args);
|
const resp = await this.apiRequest("runIntegrationTest", args);
|
||||||
if (resp.type === "response") {
|
if (resp.type === "response") {
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2020 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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import { GlobalTestState } from "./harness";
|
||||||
|
import {
|
||||||
|
createSimpleTestkudosEnvironment,
|
||||||
|
withdrawViaBank,
|
||||||
|
} from "./helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run test for basic, bank-integrated withdrawal and payment.
|
||||||
|
*/
|
||||||
|
export async function runDepositTest(t: GlobalTestState) {
|
||||||
|
// Set up test environment
|
||||||
|
|
||||||
|
const {
|
||||||
|
wallet,
|
||||||
|
bank,
|
||||||
|
exchange,
|
||||||
|
merchant,
|
||||||
|
} = await createSimpleTestkudosEnvironment(t);
|
||||||
|
|
||||||
|
// Withdraw digital cash into the wallet.
|
||||||
|
|
||||||
|
await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
|
||||||
|
|
||||||
|
await wallet.runUntilDone();
|
||||||
|
|
||||||
|
const { depositGroupId } = await wallet.createDepositGroup({
|
||||||
|
amount: "TESTKUDOS:10",
|
||||||
|
depositPaytoUri: "payto://x-taler-bank/localhost/foo",
|
||||||
|
});
|
||||||
|
|
||||||
|
await wallet.runUntilDone();
|
||||||
|
|
||||||
|
const transactions = await wallet.getTransactions();
|
||||||
|
console.log("transactions", JSON.stringify(transactions, undefined, 2));
|
||||||
|
t.assertDeepEqual(transactions.transactions[0].type, "withdrawal");
|
||||||
|
t.assertDeepEqual(transactions.transactions[1].type, "deposit");
|
||||||
|
// The raw amount is what ends up on the bank account, which includes
|
||||||
|
// deposit and wire fees.
|
||||||
|
t.assertDeepEqual(transactions.transactions[1].amountRaw, "TESTKUDOS:9.79");
|
||||||
|
|
||||||
|
const trackResult = wallet.trackDepositGroup({
|
||||||
|
depositGroupId,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(JSON.stringify(trackResult, undefined, 2));
|
||||||
|
}
|
@ -82,11 +82,6 @@ async function createTestEnvironment(
|
|||||||
database: db.connStr,
|
database: db.connStr,
|
||||||
});
|
});
|
||||||
|
|
||||||
exchange.changeConfig((config) => {
|
|
||||||
config.setString("taler-exchange-secmod-eddsa", "lookahead_sign", "20 s");
|
|
||||||
config.setString("taler-exchange-secmod-rsa", "lookahead_sign", "20 s");
|
|
||||||
});
|
|
||||||
|
|
||||||
const exchangeBankAccount = await bank.createExchangeAccount(
|
const exchangeBankAccount = await bank.createExchangeAccount(
|
||||||
"MyExchange",
|
"MyExchange",
|
||||||
"x",
|
"x",
|
||||||
|
@ -49,6 +49,7 @@ import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrat
|
|||||||
import M from "minimatch";
|
import M from "minimatch";
|
||||||
import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion";
|
import { runMerchantExchangeConfusionTest } from "./test-merchant-exchange-confusion";
|
||||||
import { runLibeufinBasicTest } from "./test-libeufin-basic";
|
import { runLibeufinBasicTest } from "./test-libeufin-basic";
|
||||||
|
import { runDepositTest } from "./test-deposit";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test runner.
|
* Test runner.
|
||||||
@ -64,6 +65,7 @@ interface TestMainFunction {
|
|||||||
const allTests: TestMainFunction[] = [
|
const allTests: TestMainFunction[] = [
|
||||||
runBankApiTest,
|
runBankApiTest,
|
||||||
runClaimLoopTest,
|
runClaimLoopTest,
|
||||||
|
runDepositTest,
|
||||||
runExchangeManagementTest,
|
runExchangeManagementTest,
|
||||||
runFeeRegressionTest,
|
runFeeRegressionTest,
|
||||||
runLibeufinBasicTest,
|
runLibeufinBasicTest,
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
DerivedTipPlanchet,
|
DerivedTipPlanchet,
|
||||||
DeriveRefreshSessionRequest,
|
DeriveRefreshSessionRequest,
|
||||||
DeriveTipRequest,
|
DeriveTipRequest,
|
||||||
|
SignTrackTransactionRequest,
|
||||||
} from "../../types/cryptoTypes";
|
} from "../../types/cryptoTypes";
|
||||||
|
|
||||||
const logger = new Logger("cryptoApi.ts");
|
const logger = new Logger("cryptoApi.ts");
|
||||||
@ -326,6 +327,10 @@ export class CryptoApi {
|
|||||||
return this.doRpc<DerivedTipPlanchet>("createTipPlanchet", 1, req);
|
return this.doRpc<DerivedTipPlanchet>("createTipPlanchet", 1, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signTrackTransaction(req: SignTrackTransactionRequest): Promise<string> {
|
||||||
|
return this.doRpc<string>("signTrackTransaction", 1, req);
|
||||||
|
}
|
||||||
|
|
||||||
hashString(str: string): Promise<string> {
|
hashString(str: string): Promise<string> {
|
||||||
return this.doRpc<string>("hashString", 1, str);
|
return this.doRpc<string>("hashString", 1, str);
|
||||||
}
|
}
|
||||||
|
@ -72,11 +72,13 @@ import {
|
|||||||
DerivedTipPlanchet,
|
DerivedTipPlanchet,
|
||||||
DeriveRefreshSessionRequest,
|
DeriveRefreshSessionRequest,
|
||||||
DeriveTipRequest,
|
DeriveTipRequest,
|
||||||
|
SignTrackTransactionRequest,
|
||||||
} from "../../types/cryptoTypes";
|
} from "../../types/cryptoTypes";
|
||||||
|
|
||||||
const logger = new Logger("cryptoImplementation.ts");
|
const logger = new Logger("cryptoImplementation.ts");
|
||||||
|
|
||||||
enum SignaturePurpose {
|
enum SignaturePurpose {
|
||||||
|
MERCHANT_TRACK_TRANSACTION = 1103,
|
||||||
WALLET_RESERVE_WITHDRAW = 1200,
|
WALLET_RESERVE_WITHDRAW = 1200,
|
||||||
WALLET_COIN_DEPOSIT = 1201,
|
WALLET_COIN_DEPOSIT = 1201,
|
||||||
MASTER_DENOMINATION_KEY_VALIDITY = 1025,
|
MASTER_DENOMINATION_KEY_VALIDITY = 1025,
|
||||||
@ -211,6 +213,16 @@ export class CryptoImplementation {
|
|||||||
return tipPlanchet;
|
return tipPlanchet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signTrackTransaction(req: SignTrackTransactionRequest): string {
|
||||||
|
const p = buildSigPS(SignaturePurpose.MERCHANT_TRACK_TRANSACTION)
|
||||||
|
.put(decodeCrock(req.contractTermsHash))
|
||||||
|
.put(decodeCrock(req.wireHash))
|
||||||
|
.put(decodeCrock(req.merchantPub))
|
||||||
|
.put(decodeCrock(req.coinPub))
|
||||||
|
.build();
|
||||||
|
return encodeCrock(eddsaSign(p, decodeCrock(req.merchantPriv)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create and sign a message to recoup a coin.
|
* Create and sign a message to recoup a coin.
|
||||||
*/
|
*/
|
||||||
|
420
packages/taler-wallet-core/src/operations/deposits.ts
Normal file
420
packages/taler-wallet-core/src/operations/deposits.ts
Normal file
@ -0,0 +1,420 @@
|
|||||||
|
/*
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
Amounts,
|
||||||
|
CreateDepositGroupRequest,
|
||||||
|
guardOperationException,
|
||||||
|
Logger,
|
||||||
|
NotificationType,
|
||||||
|
TalerErrorDetails,
|
||||||
|
} from "..";
|
||||||
|
import { kdf } from "../crypto/primitives/kdf";
|
||||||
|
import {
|
||||||
|
encodeCrock,
|
||||||
|
getRandomBytes,
|
||||||
|
stringToBytes,
|
||||||
|
} from "../crypto/talerCrypto";
|
||||||
|
import { DepositGroupRecord, Stores } from "../types/dbTypes";
|
||||||
|
import { ContractTerms } from "../types/talerTypes";
|
||||||
|
import { CreateDepositGroupResponse, TrackDepositGroupRequest, TrackDepositGroupResponse } from "../types/walletTypes";
|
||||||
|
import {
|
||||||
|
buildCodecForObject,
|
||||||
|
Codec,
|
||||||
|
codecForString,
|
||||||
|
codecOptional,
|
||||||
|
} from "../util/codec";
|
||||||
|
import { canonicalJson } from "../util/helpers";
|
||||||
|
import { readSuccessResponseJsonOrThrow } from "../util/http";
|
||||||
|
import { parsePaytoUri } from "../util/payto";
|
||||||
|
import { initRetryInfo, updateRetryInfoTimeout } from "../util/retries";
|
||||||
|
import {
|
||||||
|
codecForTimestamp,
|
||||||
|
durationFromSpec,
|
||||||
|
getTimestampNow,
|
||||||
|
Timestamp,
|
||||||
|
timestampAddDuration,
|
||||||
|
timestampTruncateToSecond,
|
||||||
|
} from "../util/time";
|
||||||
|
import { URL } from "../util/url";
|
||||||
|
import {
|
||||||
|
applyCoinSpend,
|
||||||
|
extractContractData,
|
||||||
|
generateDepositPermissions,
|
||||||
|
getCoinsForPayment,
|
||||||
|
getEffectiveDepositAmount,
|
||||||
|
getTotalPaymentCost,
|
||||||
|
} from "./pay";
|
||||||
|
import { InternalWalletState } from "./state";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger.
|
||||||
|
*/
|
||||||
|
const logger = new Logger("deposits.ts");
|
||||||
|
|
||||||
|
interface DepositSuccess {
|
||||||
|
// Optional base URL of the exchange for looking up wire transfers
|
||||||
|
// associated with this transaction. If not given,
|
||||||
|
// the base URL is the same as the one used for this request.
|
||||||
|
// Can be used if the base URL for /transactions/ differs from that
|
||||||
|
// for /coins/, i.e. for load balancing. Clients SHOULD
|
||||||
|
// respect the transaction_base_url if provided. Any HTTP server
|
||||||
|
// belonging to an exchange MUST generate a 307 or 308 redirection
|
||||||
|
// to the correct base URL should a client uses the wrong base
|
||||||
|
// URL, or if the base URL has changed since the deposit.
|
||||||
|
transaction_base_url?: string;
|
||||||
|
|
||||||
|
// timestamp when the deposit was received by the exchange.
|
||||||
|
exchange_timestamp: Timestamp;
|
||||||
|
|
||||||
|
// the EdDSA signature of TALER_DepositConfirmationPS using a current
|
||||||
|
// signing key of the exchange affirming the successful
|
||||||
|
// deposit and that the exchange will transfer the funds after the refund
|
||||||
|
// deadline, or as soon as possible if the refund deadline is zero.
|
||||||
|
exchange_sig: string;
|
||||||
|
|
||||||
|
// public EdDSA key of the exchange that was used to
|
||||||
|
// generate the signature.
|
||||||
|
// Should match one of the exchange's signing keys from /keys. It is given
|
||||||
|
// explicitly as the client might otherwise be confused by clock skew as to
|
||||||
|
// which signing key was used.
|
||||||
|
exchange_pub: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codecForDepositSuccess = (): Codec<DepositSuccess> =>
|
||||||
|
buildCodecForObject<DepositSuccess>()
|
||||||
|
.property("exchange_pub", codecForString())
|
||||||
|
.property("exchange_sig", codecForString())
|
||||||
|
.property("exchange_timestamp", codecForTimestamp)
|
||||||
|
.property("transaction_base_url", codecOptional(codecForString()))
|
||||||
|
.build("DepositSuccess");
|
||||||
|
|
||||||
|
function hashWire(paytoUri: string, salt: string): string {
|
||||||
|
const r = kdf(
|
||||||
|
64,
|
||||||
|
stringToBytes(paytoUri + "\0"),
|
||||||
|
stringToBytes(salt + "\0"),
|
||||||
|
stringToBytes("merchant-wire-signature"),
|
||||||
|
);
|
||||||
|
return encodeCrock(r);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resetDepositGroupRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
depositGroupId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db.mutate(Stores.depositGroups, depositGroupId, (x) => {
|
||||||
|
if (x.retryInfo.active) {
|
||||||
|
x.retryInfo = initRetryInfo();
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function incrementDepositRetry(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
depositGroupId: string,
|
||||||
|
err: TalerErrorDetails | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => {
|
||||||
|
const r = await tx.get(Stores.depositGroups, depositGroupId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.retryInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
r.retryInfo.retryCounter++;
|
||||||
|
updateRetryInfoTimeout(r.retryInfo);
|
||||||
|
r.lastError = err;
|
||||||
|
await tx.put(Stores.depositGroups, r);
|
||||||
|
});
|
||||||
|
if (err) {
|
||||||
|
ws.notify({ type: NotificationType.DepositOperationError, error: err });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processDepositGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
depositGroupId: string,
|
||||||
|
forceNow = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await ws.memoProcessDeposit.memo(depositGroupId, async () => {
|
||||||
|
const onOpErr = (e: TalerErrorDetails): Promise<void> =>
|
||||||
|
incrementDepositRetry(ws, depositGroupId, e);
|
||||||
|
return await guardOperationException(
|
||||||
|
async () => await processDepositGroupImpl(ws, depositGroupId, forceNow),
|
||||||
|
onOpErr,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processDepositGroupImpl(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
depositGroupId: string,
|
||||||
|
forceNow: boolean = false,
|
||||||
|
): Promise<void> {
|
||||||
|
if (forceNow) {
|
||||||
|
await resetDepositGroupRetry(ws, depositGroupId);
|
||||||
|
}
|
||||||
|
const depositGroup = await ws.db.get(Stores.depositGroups, depositGroupId);
|
||||||
|
if (!depositGroup) {
|
||||||
|
logger.warn(`deposit group ${depositGroupId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (depositGroup.timestampFinished) {
|
||||||
|
logger.trace(`deposit group ${depositGroupId} already finished`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contractData = extractContractData(
|
||||||
|
depositGroup.contractTermsRaw,
|
||||||
|
depositGroup.contractTermsHash,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const depositPermissions = await generateDepositPermissions(
|
||||||
|
ws,
|
||||||
|
depositGroup.payCoinSelection,
|
||||||
|
contractData,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < depositPermissions.length; i++) {
|
||||||
|
if (depositGroup.depositedPerCoin[i]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const perm = depositPermissions[i];
|
||||||
|
const url = new URL(`/coins/${perm.coin_pub}/deposit`, perm.exchange_url);
|
||||||
|
const httpResp = await ws.http.postJson(url.href, {
|
||||||
|
contribution: Amounts.stringify(perm.contribution),
|
||||||
|
wire: depositGroup.wire,
|
||||||
|
h_wire: depositGroup.contractTermsRaw.h_wire,
|
||||||
|
h_contract_terms: depositGroup.contractTermsHash,
|
||||||
|
ub_sig: perm.ub_sig,
|
||||||
|
timestamp: depositGroup.contractTermsRaw.timestamp,
|
||||||
|
wire_transfer_deadline:
|
||||||
|
depositGroup.contractTermsRaw.wire_transfer_deadline,
|
||||||
|
refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
|
||||||
|
coin_sig: perm.coin_sig,
|
||||||
|
denom_pub_hash: perm.h_denom,
|
||||||
|
merchant_pub: depositGroup.merchantPub,
|
||||||
|
});
|
||||||
|
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
|
||||||
|
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => {
|
||||||
|
const dg = await tx.get(Stores.depositGroups, depositGroupId);
|
||||||
|
if (!dg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dg.depositedPerCoin[i] = true;
|
||||||
|
await tx.put(Stores.depositGroups, dg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.db.runWithWriteTransaction([Stores.depositGroups], async (tx) => {
|
||||||
|
const dg = await tx.get(Stores.depositGroups, depositGroupId);
|
||||||
|
if (!dg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let allDeposited = true;
|
||||||
|
for (const d of depositGroup.depositedPerCoin) {
|
||||||
|
if (!d) {
|
||||||
|
allDeposited = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (allDeposited) {
|
||||||
|
dg.timestampFinished = getTimestampNow();
|
||||||
|
await tx.put(Stores.depositGroups, dg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function trackDepositGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: TrackDepositGroupRequest,
|
||||||
|
): Promise<TrackDepositGroupResponse> {
|
||||||
|
const responses: {
|
||||||
|
status: number;
|
||||||
|
body: any;
|
||||||
|
}[] = [];
|
||||||
|
const depositGroup = await ws.db.get(
|
||||||
|
Stores.depositGroups,
|
||||||
|
req.depositGroupId,
|
||||||
|
);
|
||||||
|
if (!depositGroup) {
|
||||||
|
throw Error("deposit group not found");
|
||||||
|
}
|
||||||
|
const contractData = extractContractData(
|
||||||
|
depositGroup.contractTermsRaw,
|
||||||
|
depositGroup.contractTermsHash,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const depositPermissions = await generateDepositPermissions(
|
||||||
|
ws,
|
||||||
|
depositGroup.payCoinSelection,
|
||||||
|
contractData,
|
||||||
|
);
|
||||||
|
|
||||||
|
const wireHash = depositGroup.contractTermsRaw.h_wire;
|
||||||
|
|
||||||
|
for (const dp of depositPermissions) {
|
||||||
|
const url = new URL(
|
||||||
|
`/deposits/${wireHash}/${depositGroup.merchantPub}/${depositGroup.contractTermsHash}/${dp.coin_pub}`,
|
||||||
|
dp.exchange_url,
|
||||||
|
);
|
||||||
|
const sig = await ws.cryptoApi.signTrackTransaction({
|
||||||
|
coinPub: dp.coin_pub,
|
||||||
|
contractTermsHash: depositGroup.contractTermsHash,
|
||||||
|
merchantPriv: depositGroup.merchantPriv,
|
||||||
|
merchantPub: depositGroup.merchantPub,
|
||||||
|
wireHash,
|
||||||
|
});
|
||||||
|
url.searchParams.set("merchant_sig", sig);
|
||||||
|
const httpResp = await ws.http.get(url.href);
|
||||||
|
const body = await httpResp.json();
|
||||||
|
responses.push({
|
||||||
|
body,
|
||||||
|
status: httpResp.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
responses,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createDepositGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: CreateDepositGroupRequest,
|
||||||
|
): Promise<CreateDepositGroupResponse> {
|
||||||
|
const p = parsePaytoUri(req.depositPaytoUri);
|
||||||
|
if (!p) {
|
||||||
|
throw Error("invalid payto URI");
|
||||||
|
}
|
||||||
|
|
||||||
|
const amount = Amounts.parseOrThrow(req.amount);
|
||||||
|
|
||||||
|
const allExchanges = await ws.db.iter(Stores.exchanges).toArray();
|
||||||
|
const exchangeInfos: { url: string; master_pub: string }[] = [];
|
||||||
|
for (const e of allExchanges) {
|
||||||
|
if (!e.details) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (e.details.currency != amount.currency) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
exchangeInfos.push({
|
||||||
|
master_pub: e.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(64));
|
||||||
|
const wireHash = hashWire(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: noncePair.pub,
|
||||||
|
wire_transfer_deadline: timestampRound,
|
||||||
|
order_id: "",
|
||||||
|
h_wire: wireHash,
|
||||||
|
pay_deadline: timestampAddDuration(
|
||||||
|
timestampRound,
|
||||||
|
durationFromSpec({ hours: 1 }),
|
||||||
|
),
|
||||||
|
merchant: {
|
||||||
|
name: "",
|
||||||
|
},
|
||||||
|
merchant_pub: merchantPair.pub,
|
||||||
|
refund_deadline: { t_ms: 0 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const contractTermsHash = await ws.cryptoApi.hashString(
|
||||||
|
canonicalJson(contractTerms),
|
||||||
|
);
|
||||||
|
|
||||||
|
const contractData = extractContractData(
|
||||||
|
contractTerms,
|
||||||
|
contractTermsHash,
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
|
||||||
|
const payCoinSel = await getCoinsForPayment(ws, contractData);
|
||||||
|
|
||||||
|
if (!payCoinSel) {
|
||||||
|
throw Error("insufficient funds");
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalDepositCost = await getTotalPaymentCost(ws, payCoinSel);
|
||||||
|
|
||||||
|
const depositGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
|
const effectiveDepositAmount = await getEffectiveDepositAmount(
|
||||||
|
ws,
|
||||||
|
p.targetType,
|
||||||
|
payCoinSel,
|
||||||
|
);
|
||||||
|
|
||||||
|
const depositGroup: DepositGroupRecord = {
|
||||||
|
contractTermsHash,
|
||||||
|
contractTermsRaw: contractTerms,
|
||||||
|
depositGroupId,
|
||||||
|
noncePriv: noncePair.priv,
|
||||||
|
noncePub: noncePair.pub,
|
||||||
|
timestampCreated: timestamp,
|
||||||
|
timestampFinished: undefined,
|
||||||
|
payCoinSelection: payCoinSel,
|
||||||
|
depositedPerCoin: payCoinSel.coinPubs.map((x) => false),
|
||||||
|
merchantPriv: merchantPair.priv,
|
||||||
|
merchantPub: merchantPair.pub,
|
||||||
|
totalPayCost: totalDepositCost,
|
||||||
|
effectiveDepositAmount,
|
||||||
|
wire: {
|
||||||
|
payto_uri: req.depositPaytoUri,
|
||||||
|
salt: wireSalt,
|
||||||
|
},
|
||||||
|
retryInfo: initRetryInfo(true),
|
||||||
|
lastError: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
await ws.db.runWithWriteTransaction(
|
||||||
|
[
|
||||||
|
Stores.depositGroups,
|
||||||
|
Stores.coins,
|
||||||
|
Stores.refreshGroups,
|
||||||
|
Stores.denominations,
|
||||||
|
],
|
||||||
|
async (tx) => {
|
||||||
|
await applyCoinSpend(ws, tx, payCoinSel);
|
||||||
|
await tx.put(Stores.depositGroups, depositGroup);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await ws.db.put(Stores.depositGroups, depositGroup);
|
||||||
|
|
||||||
|
return { depositGroupId };
|
||||||
|
}
|
@ -36,6 +36,8 @@ import {
|
|||||||
DenominationRecord,
|
DenominationRecord,
|
||||||
PayCoinSelection,
|
PayCoinSelection,
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
|
AllowedExchangeInfo,
|
||||||
|
AllowedAuditorInfo,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { NotificationType } from "../types/notifications";
|
import { NotificationType } from "../types/notifications";
|
||||||
import {
|
import {
|
||||||
@ -43,6 +45,7 @@ import {
|
|||||||
codecForContractTerms,
|
codecForContractTerms,
|
||||||
CoinDepositPermission,
|
CoinDepositPermission,
|
||||||
codecForMerchantPayResponse,
|
codecForMerchantPayResponse,
|
||||||
|
ContractTerms,
|
||||||
} from "../types/talerTypes";
|
} from "../types/talerTypes";
|
||||||
import {
|
import {
|
||||||
ConfirmPayResult,
|
ConfirmPayResult,
|
||||||
@ -72,7 +75,8 @@ import {
|
|||||||
durationMin,
|
durationMin,
|
||||||
isTimestampExpired,
|
isTimestampExpired,
|
||||||
durationMul,
|
durationMul,
|
||||||
durationAdd,
|
Timestamp,
|
||||||
|
timestampIsBetween,
|
||||||
} from "../util/time";
|
} from "../util/time";
|
||||||
import { strcmp, canonicalJson } from "../util/helpers";
|
import { strcmp, canonicalJson } from "../util/helpers";
|
||||||
import {
|
import {
|
||||||
@ -88,6 +92,7 @@ import {
|
|||||||
updateRetryInfoTimeout,
|
updateRetryInfoTimeout,
|
||||||
getRetryDuration,
|
getRetryDuration,
|
||||||
} from "../util/retries";
|
} from "../util/retries";
|
||||||
|
import { TransactionHandle } from "../util/query";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger.
|
* Logger.
|
||||||
@ -162,6 +167,49 @@ export async function getTotalPaymentCost(
|
|||||||
return Amounts.sum(costs).amount;
|
return Amounts.sum(costs).amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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();
|
||||||
|
for (let i = 0; i < pcs.coinPubs.length; i++) {
|
||||||
|
const coin = await ws.db.get(Stores.coins, pcs.coinPubs[i]);
|
||||||
|
if (!coin) {
|
||||||
|
throw Error("can't calculate deposit amountt, coin not found");
|
||||||
|
}
|
||||||
|
const denom = await ws.db.get(Stores.denominations, [
|
||||||
|
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 exchange = await ws.db.get(Stores.exchanges, exchangeUrl);
|
||||||
|
if (!exchange?.wireInfo) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const fee = exchange.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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a list of available coins, select coins to spend under the merchant's
|
* Given a list of available coins, select coins to spend under the merchant's
|
||||||
* constraints.
|
* constraints.
|
||||||
@ -277,17 +325,36 @@ export function isSpendableCoin(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CoinSelectionRequest {
|
||||||
|
amount: AmountJson;
|
||||||
|
allowedAuditors: AllowedAuditorInfo[];
|
||||||
|
allowedExchanges: AllowedExchangeInfo[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timestamp of the contract.
|
||||||
|
*/
|
||||||
|
timestamp: Timestamp;
|
||||||
|
|
||||||
|
wireMethod: string;
|
||||||
|
|
||||||
|
wireFeeAmortization: number;
|
||||||
|
|
||||||
|
maxWireFee: AmountJson;
|
||||||
|
|
||||||
|
maxDepositFee: AmountJson;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select coins from the wallet's database that can be used
|
* Select coins from the wallet's database that can be used
|
||||||
* to pay for the given contract.
|
* to pay for the given contract.
|
||||||
*
|
*
|
||||||
* If payment is impossible, undefined is returned.
|
* If payment is impossible, undefined is returned.
|
||||||
*/
|
*/
|
||||||
async function getCoinsForPayment(
|
export async function getCoinsForPayment(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
contractData: WalletContractData,
|
req: CoinSelectionRequest,
|
||||||
): Promise<PayCoinSelection | undefined> {
|
): Promise<PayCoinSelection | undefined> {
|
||||||
const remainingAmount = contractData.amount;
|
const remainingAmount = req.amount;
|
||||||
|
|
||||||
const exchanges = await ws.db.iter(Stores.exchanges).toArray();
|
const exchanges = await ws.db.iter(Stores.exchanges).toArray();
|
||||||
|
|
||||||
@ -303,7 +370,7 @@ async function getCoinsForPayment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// is the exchange explicitly allowed?
|
// is the exchange explicitly allowed?
|
||||||
for (const allowedExchange of contractData.allowedExchanges) {
|
for (const allowedExchange of req.allowedExchanges) {
|
||||||
if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
|
if (allowedExchange.exchangePub === exchangeDetails.masterPublicKey) {
|
||||||
isOkay = true;
|
isOkay = true;
|
||||||
break;
|
break;
|
||||||
@ -312,7 +379,7 @@ async function getCoinsForPayment(
|
|||||||
|
|
||||||
// is the exchange allowed because of one of its auditors?
|
// is the exchange allowed because of one of its auditors?
|
||||||
if (!isOkay) {
|
if (!isOkay) {
|
||||||
for (const allowedAuditor of contractData.allowedAuditors) {
|
for (const allowedAuditor of req.allowedAuditors) {
|
||||||
for (const auditor of exchangeDetails.auditors) {
|
for (const auditor of exchangeDetails.auditors) {
|
||||||
if (auditor.auditor_pub === allowedAuditor.auditorPub) {
|
if (auditor.auditor_pub === allowedAuditor.auditorPub) {
|
||||||
isOkay = true;
|
isOkay = true;
|
||||||
@ -374,11 +441,8 @@ async function getCoinsForPayment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let wireFee: AmountJson | undefined;
|
let wireFee: AmountJson | undefined;
|
||||||
for (const fee of exchangeFees.feesForType[contractData.wireMethod] || []) {
|
for (const fee of exchangeFees.feesForType[req.wireMethod] || []) {
|
||||||
if (
|
if (fee.startStamp <= req.timestamp && fee.endStamp >= req.timestamp) {
|
||||||
fee.startStamp <= contractData.timestamp &&
|
|
||||||
fee.endStamp >= contractData.timestamp
|
|
||||||
) {
|
|
||||||
wireFee = fee.wireFee;
|
wireFee = fee.wireFee;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -386,12 +450,9 @@ async function getCoinsForPayment(
|
|||||||
|
|
||||||
let customerWireFee: AmountJson;
|
let customerWireFee: AmountJson;
|
||||||
|
|
||||||
if (wireFee) {
|
if (wireFee && req.wireFeeAmortization) {
|
||||||
const amortizedWireFee = Amounts.divide(
|
const amortizedWireFee = Amounts.divide(wireFee, req.wireFeeAmortization);
|
||||||
wireFee,
|
if (Amounts.cmp(req.maxWireFee, amortizedWireFee) < 0) {
|
||||||
contractData.wireFeeAmortization,
|
|
||||||
);
|
|
||||||
if (Amounts.cmp(contractData.maxWireFee, amortizedWireFee) < 0) {
|
|
||||||
customerWireFee = amortizedWireFee;
|
customerWireFee = amortizedWireFee;
|
||||||
} else {
|
} else {
|
||||||
customerWireFee = Amounts.getZero(currency);
|
customerWireFee = Amounts.getZero(currency);
|
||||||
@ -405,7 +466,7 @@ async function getCoinsForPayment(
|
|||||||
acis,
|
acis,
|
||||||
remainingAmount,
|
remainingAmount,
|
||||||
customerWireFee,
|
customerWireFee,
|
||||||
contractData.maxDepositFee,
|
req.maxDepositFee,
|
||||||
);
|
);
|
||||||
if (res) {
|
if (res) {
|
||||||
return res;
|
return res;
|
||||||
@ -414,6 +475,37 @@ async function getCoinsForPayment(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function applyCoinSpend(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
tx: TransactionHandle<
|
||||||
|
| typeof Stores.coins
|
||||||
|
| typeof Stores.refreshGroups
|
||||||
|
| typeof Stores.denominations
|
||||||
|
>,
|
||||||
|
coinSelection: PayCoinSelection,
|
||||||
|
) {
|
||||||
|
for (let i = 0; i < coinSelection.coinPubs.length; i++) {
|
||||||
|
const coin = await tx.get(Stores.coins, coinSelection.coinPubs[i]);
|
||||||
|
if (!coin) {
|
||||||
|
throw Error("coin allocated for payment doesn't exist anymore");
|
||||||
|
}
|
||||||
|
coin.status = CoinStatus.Dormant;
|
||||||
|
const remaining = Amounts.sub(
|
||||||
|
coin.currentAmount,
|
||||||
|
coinSelection.coinContributions[i],
|
||||||
|
);
|
||||||
|
if (remaining.saturated) {
|
||||||
|
throw Error("not enough remaining balance on coin for payment");
|
||||||
|
}
|
||||||
|
coin.currentAmount = remaining.amount;
|
||||||
|
await tx.put(Stores.coins, coin);
|
||||||
|
}
|
||||||
|
const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
|
||||||
|
coinPub: x,
|
||||||
|
}));
|
||||||
|
await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.Pay);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Record all information that is necessary to
|
* Record all information that is necessary to
|
||||||
* pay for a proposal in the wallet's database.
|
* pay for a proposal in the wallet's database.
|
||||||
@ -480,26 +572,7 @@ async function recordConfirmPay(
|
|||||||
await tx.put(Stores.proposals, p);
|
await tx.put(Stores.proposals, p);
|
||||||
}
|
}
|
||||||
await tx.put(Stores.purchases, t);
|
await tx.put(Stores.purchases, t);
|
||||||
for (let i = 0; i < coinSelection.coinPubs.length; i++) {
|
await applyCoinSpend(ws, tx, coinSelection);
|
||||||
const coin = await tx.get(Stores.coins, coinSelection.coinPubs[i]);
|
|
||||||
if (!coin) {
|
|
||||||
throw Error("coin allocated for payment doesn't exist anymore");
|
|
||||||
}
|
|
||||||
coin.status = CoinStatus.Dormant;
|
|
||||||
const remaining = Amounts.sub(
|
|
||||||
coin.currentAmount,
|
|
||||||
coinSelection.coinContributions[i],
|
|
||||||
);
|
|
||||||
if (remaining.saturated) {
|
|
||||||
throw Error("not enough remaining balance on coin for payment");
|
|
||||||
}
|
|
||||||
coin.currentAmount = remaining.amount;
|
|
||||||
await tx.put(Stores.coins, coin);
|
|
||||||
}
|
|
||||||
const refreshCoinPubs = coinSelection.coinPubs.map((x) => ({
|
|
||||||
coinPub: x,
|
|
||||||
}));
|
|
||||||
await createRefreshGroup(ws, tx, refreshCoinPubs, RefreshReason.Pay);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -609,6 +682,50 @@ function getPayRequestTimeout(purchase: PurchaseRecord): Duration {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractContractData(
|
||||||
|
parsedContractTerms: ContractTerms,
|
||||||
|
contractTermsHash: string,
|
||||||
|
merchantSig: string,
|
||||||
|
): WalletContractData {
|
||||||
|
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||||
|
let maxWireFee: AmountJson;
|
||||||
|
if (parsedContractTerms.max_wire_fee) {
|
||||||
|
maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
|
||||||
|
} else {
|
||||||
|
maxWireFee = Amounts.getZero(amount.currency);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
amount,
|
||||||
|
contractTermsHash: contractTermsHash,
|
||||||
|
fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
|
||||||
|
merchantBaseUrl: parsedContractTerms.merchant_base_url,
|
||||||
|
merchantPub: parsedContractTerms.merchant_pub,
|
||||||
|
merchantSig,
|
||||||
|
orderId: parsedContractTerms.order_id,
|
||||||
|
summary: parsedContractTerms.summary,
|
||||||
|
autoRefund: parsedContractTerms.auto_refund,
|
||||||
|
maxWireFee,
|
||||||
|
payDeadline: parsedContractTerms.pay_deadline,
|
||||||
|
refundDeadline: parsedContractTerms.refund_deadline,
|
||||||
|
wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1,
|
||||||
|
allowedAuditors: parsedContractTerms.auditors.map((x) => ({
|
||||||
|
auditorBaseUrl: x.url,
|
||||||
|
auditorPub: x.auditor_pub,
|
||||||
|
})),
|
||||||
|
allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
|
||||||
|
exchangeBaseUrl: x.url,
|
||||||
|
exchangePub: x.master_pub,
|
||||||
|
})),
|
||||||
|
timestamp: parsedContractTerms.timestamp,
|
||||||
|
wireMethod: parsedContractTerms.wire_method,
|
||||||
|
wireInfoHash: parsedContractTerms.h_wire,
|
||||||
|
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
|
||||||
|
merchant: parsedContractTerms.merchant,
|
||||||
|
products: parsedContractTerms.products,
|
||||||
|
summaryI18n: parsedContractTerms.summary_i18n,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function processDownloadProposalImpl(
|
async function processDownloadProposalImpl(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
proposalId: string,
|
proposalId: string,
|
||||||
@ -714,6 +831,12 @@ async function processDownloadProposalImpl(
|
|||||||
throw new OperationFailedAndReportedError(err);
|
throw new OperationFailedAndReportedError(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const contractData = extractContractData(
|
||||||
|
parsedContractTerms,
|
||||||
|
contractTermsHash,
|
||||||
|
proposalResp.sig,
|
||||||
|
);
|
||||||
|
|
||||||
await ws.db.runWithWriteTransaction(
|
await ws.db.runWithWriteTransaction(
|
||||||
[Stores.proposals, Stores.purchases],
|
[Stores.proposals, Stores.purchases],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
@ -724,44 +847,8 @@ async function processDownloadProposalImpl(
|
|||||||
if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
|
if (p.proposalStatus !== ProposalStatus.DOWNLOADING) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
|
||||||
let maxWireFee: AmountJson;
|
|
||||||
if (parsedContractTerms.max_wire_fee) {
|
|
||||||
maxWireFee = Amounts.parseOrThrow(parsedContractTerms.max_wire_fee);
|
|
||||||
} else {
|
|
||||||
maxWireFee = Amounts.getZero(amount.currency);
|
|
||||||
}
|
|
||||||
p.download = {
|
p.download = {
|
||||||
contractData: {
|
contractData,
|
||||||
amount,
|
|
||||||
contractTermsHash: contractTermsHash,
|
|
||||||
fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
|
|
||||||
merchantBaseUrl: parsedContractTerms.merchant_base_url,
|
|
||||||
merchantPub: parsedContractTerms.merchant_pub,
|
|
||||||
merchantSig: proposalResp.sig,
|
|
||||||
orderId: parsedContractTerms.order_id,
|
|
||||||
summary: parsedContractTerms.summary,
|
|
||||||
autoRefund: parsedContractTerms.auto_refund,
|
|
||||||
maxWireFee,
|
|
||||||
payDeadline: parsedContractTerms.pay_deadline,
|
|
||||||
refundDeadline: parsedContractTerms.refund_deadline,
|
|
||||||
wireFeeAmortization: parsedContractTerms.wire_fee_amortization || 1,
|
|
||||||
allowedAuditors: parsedContractTerms.auditors.map((x) => ({
|
|
||||||
auditorBaseUrl: x.url,
|
|
||||||
auditorPub: x.auditor_pub,
|
|
||||||
})),
|
|
||||||
allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
|
|
||||||
exchangeBaseUrl: x.url,
|
|
||||||
exchangePub: x.master_pub,
|
|
||||||
})),
|
|
||||||
timestamp: parsedContractTerms.timestamp,
|
|
||||||
wireMethod: parsedContractTerms.wire_method,
|
|
||||||
wireInfoHash: parsedContractTerms.h_wire,
|
|
||||||
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
|
|
||||||
merchant: parsedContractTerms.merchant,
|
|
||||||
products: parsedContractTerms.products,
|
|
||||||
summaryI18n: parsedContractTerms.summary_i18n,
|
|
||||||
},
|
|
||||||
contractTermsRaw: proposalResp.contract_terms,
|
contractTermsRaw: proposalResp.contract_terms,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
@ -1210,7 +1297,7 @@ export async function preparePayForUri(
|
|||||||
*
|
*
|
||||||
* Accesses the database and the crypto worker.
|
* Accesses the database and the crypto worker.
|
||||||
*/
|
*/
|
||||||
async function generateDepositPermissions(
|
export async function generateDepositPermissions(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
payCoinSel: PayCoinSelection,
|
payCoinSel: PayCoinSelection,
|
||||||
contractData: WalletContractData,
|
contractData: WalletContractData,
|
||||||
|
@ -445,6 +445,34 @@ async function gatherRecoupPending(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function gatherDepositPending(
|
||||||
|
tx: TransactionHandle<typeof Stores.depositGroups>,
|
||||||
|
now: Timestamp,
|
||||||
|
resp: PendingOperationsResponse,
|
||||||
|
onlyDue = false,
|
||||||
|
): Promise<void> {
|
||||||
|
await tx.iter(Stores.depositGroups).forEach((dg) => {
|
||||||
|
if (dg.timestampFinished) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.nextRetryDelay = updateRetryDelay(
|
||||||
|
resp.nextRetryDelay,
|
||||||
|
now,
|
||||||
|
dg.retryInfo.nextRetry,
|
||||||
|
);
|
||||||
|
if (onlyDue && dg.retryInfo.nextRetry.t_ms > now.t_ms) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resp.pendingOperations.push({
|
||||||
|
type: PendingOperationType.Deposit,
|
||||||
|
givesLifeness: true,
|
||||||
|
depositGroupId: dg.depositGroupId,
|
||||||
|
retryInfo: dg.retryInfo,
|
||||||
|
lastError: dg.lastError,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function getPendingOperations(
|
export async function getPendingOperations(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
{ onlyDue = false } = {},
|
{ onlyDue = false } = {},
|
||||||
@ -462,6 +490,7 @@ export async function getPendingOperations(
|
|||||||
Stores.purchases,
|
Stores.purchases,
|
||||||
Stores.recoupGroups,
|
Stores.recoupGroups,
|
||||||
Stores.planchets,
|
Stores.planchets,
|
||||||
|
Stores.depositGroups,
|
||||||
],
|
],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
const walletBalance = await getBalancesInsideTransaction(ws, tx);
|
||||||
@ -479,6 +508,7 @@ export async function getPendingOperations(
|
|||||||
await gatherTipPending(tx, now, resp, onlyDue);
|
await gatherTipPending(tx, now, resp, onlyDue);
|
||||||
await gatherPurchasePending(tx, now, resp, onlyDue);
|
await gatherPurchasePending(tx, now, resp, onlyDue);
|
||||||
await gatherRecoupPending(tx, now, resp, onlyDue);
|
await gatherRecoupPending(tx, now, resp, onlyDue);
|
||||||
|
await gatherDepositPending(tx, now, resp, onlyDue);
|
||||||
return resp;
|
return resp;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -601,6 +601,7 @@ async function processPurchaseQueryRefundImpl(
|
|||||||
purchase.download.contractData.merchantBaseUrl,
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
logger.trace(`making refund request to ${requestUrl.href}`);
|
logger.trace(`making refund request to ${requestUrl.href}`);
|
||||||
|
|
||||||
const request = await ws.http.postJson(requestUrl.href, {
|
const request = await ws.http.postJson(requestUrl.href, {
|
||||||
|
@ -41,6 +41,7 @@ export class InternalWalletState {
|
|||||||
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
|
memoGetBalance: AsyncOpMemoSingle<BalancesResponse> = new AsyncOpMemoSingle();
|
||||||
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessRefresh: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
memoProcessRecoup: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
|
memoProcessDeposit: AsyncOpMemoMap<void> = new AsyncOpMemoMap();
|
||||||
cryptoApi: CryptoApi;
|
cryptoApi: CryptoApi;
|
||||||
|
|
||||||
listeners: NotificationListener[] = [];
|
listeners: NotificationListener[] = [];
|
||||||
|
@ -96,6 +96,7 @@ export async function getTransactions(
|
|||||||
Stores.withdrawalGroups,
|
Stores.withdrawalGroups,
|
||||||
Stores.planchets,
|
Stores.planchets,
|
||||||
Stores.recoupGroups,
|
Stores.recoupGroups,
|
||||||
|
Stores.depositGroups,
|
||||||
],
|
],
|
||||||
// Report withdrawals that are currently in progress.
|
// Report withdrawals that are currently in progress.
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
@ -203,6 +204,28 @@ export async function getTransactions(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
tx.iter(Stores.depositGroups).forEachAsync(async (dg) => {
|
||||||
|
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
|
||||||
|
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
transactions.push({
|
||||||
|
type: TransactionType.Deposit,
|
||||||
|
amountRaw: Amounts.stringify(dg.effectiveDepositAmount),
|
||||||
|
amountEffective: Amounts.stringify(dg.totalPayCost),
|
||||||
|
pending: !dg.timestampFinished,
|
||||||
|
timestamp: dg.timestampCreated,
|
||||||
|
targetPaytoUri: dg.wire.payto_uri,
|
||||||
|
transactionId: makeEventId(
|
||||||
|
TransactionType.Deposit,
|
||||||
|
dg.depositGroupId,
|
||||||
|
),
|
||||||
|
depositGroupId: dg.depositGroupId,
|
||||||
|
...(dg.lastError ? { error: dg.lastError } : {}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
tx.iter(Stores.purchases).forEachAsync(async (pr) => {
|
tx.iter(Stores.purchases).forEachAsync(async (pr) => {
|
||||||
if (
|
if (
|
||||||
shouldSkipCurrency(
|
shouldSkipCurrency(
|
||||||
|
@ -131,3 +131,11 @@ export interface DerivedTipPlanchet {
|
|||||||
coinPriv: string;
|
coinPriv: string;
|
||||||
coinPub: string;
|
coinPub: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SignTrackTransactionRequest {
|
||||||
|
contractTermsHash: string;
|
||||||
|
wireHash: string;
|
||||||
|
coinPub: string;
|
||||||
|
merchantPriv: string;
|
||||||
|
merchantPub: string;
|
||||||
|
}
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
Product,
|
Product,
|
||||||
InternationalizedString,
|
InternationalizedString,
|
||||||
AmountString,
|
AmountString,
|
||||||
|
ContractTerms,
|
||||||
} from "./talerTypes";
|
} from "./talerTypes";
|
||||||
|
|
||||||
import { Index, Store } from "../util/query";
|
import { Index, Store } from "../util/query";
|
||||||
@ -1481,6 +1482,54 @@ export interface BackupProviderRecord {
|
|||||||
lastError: TalerErrorDetails | undefined;
|
lastError: TalerErrorDetails | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group of deposits made by the wallet.
|
||||||
|
*/
|
||||||
|
export interface DepositGroupRecord {
|
||||||
|
depositGroupId: string;
|
||||||
|
|
||||||
|
merchantPub: string;
|
||||||
|
merchantPriv: string;
|
||||||
|
|
||||||
|
noncePriv: string;
|
||||||
|
noncePub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire information used by all deposits in this
|
||||||
|
* deposit group.
|
||||||
|
*/
|
||||||
|
wire: {
|
||||||
|
payto_uri: string;
|
||||||
|
salt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verbatim contract terms.
|
||||||
|
*/
|
||||||
|
contractTermsRaw: ContractTerms;
|
||||||
|
|
||||||
|
contractTermsHash: string;
|
||||||
|
|
||||||
|
payCoinSelection: PayCoinSelection;
|
||||||
|
|
||||||
|
totalPayCost: AmountJson;
|
||||||
|
|
||||||
|
effectiveDepositAmount: AmountJson;
|
||||||
|
|
||||||
|
depositedPerCoin: boolean[];
|
||||||
|
|
||||||
|
timestampCreated: Timestamp;
|
||||||
|
|
||||||
|
timestampFinished: Timestamp | undefined;
|
||||||
|
|
||||||
|
lastError: TalerErrorDetails | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info.
|
||||||
|
*/
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
|
class ExchangesStore extends Store<"exchanges", ExchangeRecord> {
|
||||||
constructor() {
|
constructor() {
|
||||||
super("exchanges", { keyPath: "baseUrl" });
|
super("exchanges", { keyPath: "baseUrl" });
|
||||||
@ -1657,6 +1706,12 @@ class BackupProvidersStore extends Store<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class DepositGroupsStore extends Store<"depositGroups", DepositGroupRecord> {
|
||||||
|
constructor() {
|
||||||
|
super("depositGroups", { keyPath: "depositGroupId" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The stores and indices for the wallet database.
|
* The stores and indices for the wallet database.
|
||||||
*/
|
*/
|
||||||
@ -1683,6 +1738,7 @@ export const Stores = {
|
|||||||
planchets: new PlanchetsStore(),
|
planchets: new PlanchetsStore(),
|
||||||
bankWithdrawUris: new BankWithdrawUrisStore(),
|
bankWithdrawUris: new BankWithdrawUrisStore(),
|
||||||
backupProviders: new BackupProvidersStore(),
|
backupProviders: new BackupProvidersStore(),
|
||||||
|
depositGroups: new DepositGroupsStore(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export class MetaConfigStore extends Store<"metaConfig", ConfigRecord<any>> {
|
export class MetaConfigStore extends Store<"metaConfig", ConfigRecord<any>> {
|
||||||
|
@ -60,6 +60,7 @@ export enum NotificationType {
|
|||||||
PendingOperationProcessed = "pending-operation-processed",
|
PendingOperationProcessed = "pending-operation-processed",
|
||||||
ProposalRefused = "proposal-refused",
|
ProposalRefused = "proposal-refused",
|
||||||
ReserveRegisteredWithBank = "reserve-registered-with-bank",
|
ReserveRegisteredWithBank = "reserve-registered-with-bank",
|
||||||
|
DepositOperationError = "deposit-operation-error",
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProposalAcceptedNotification {
|
export interface ProposalAcceptedNotification {
|
||||||
@ -193,6 +194,11 @@ export interface RecoupOperationErrorNotification {
|
|||||||
error: TalerErrorDetails;
|
error: TalerErrorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DepositOperationErrorNotification {
|
||||||
|
type: NotificationType.DepositOperationError;
|
||||||
|
error: TalerErrorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ReserveOperationErrorNotification {
|
export interface ReserveOperationErrorNotification {
|
||||||
type: NotificationType.ReserveOperationError;
|
type: NotificationType.ReserveOperationError;
|
||||||
error: TalerErrorDetails;
|
error: TalerErrorDetails;
|
||||||
@ -256,6 +262,7 @@ export type WalletNotification =
|
|||||||
| WithdrawalGroupCreatedNotification
|
| WithdrawalGroupCreatedNotification
|
||||||
| CoinWithdrawnNotification
|
| CoinWithdrawnNotification
|
||||||
| RecoupOperationErrorNotification
|
| RecoupOperationErrorNotification
|
||||||
|
| DepositOperationErrorNotification
|
||||||
| InternalErrorNotification
|
| InternalErrorNotification
|
||||||
| PendingOperationProcessedNotification
|
| PendingOperationProcessedNotification
|
||||||
| ProposalRefusedNotification
|
| ProposalRefusedNotification
|
||||||
|
@ -40,6 +40,7 @@ export enum PendingOperationType {
|
|||||||
TipChoice = "tip-choice",
|
TipChoice = "tip-choice",
|
||||||
TipPickup = "tip-pickup",
|
TipPickup = "tip-pickup",
|
||||||
Withdraw = "withdraw",
|
Withdraw = "withdraw",
|
||||||
|
Deposit = "deposit",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -60,6 +61,7 @@ export type PendingOperationInfo = PendingOperationInfoCommon &
|
|||||||
| PendingTipPickupOperation
|
| PendingTipPickupOperation
|
||||||
| PendingWithdrawOperation
|
| PendingWithdrawOperation
|
||||||
| PendingRecoupOperation
|
| PendingRecoupOperation
|
||||||
|
| PendingDepositOperation
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -227,6 +229,16 @@ export interface PendingWithdrawOperation {
|
|||||||
numCoinsTotal: number;
|
numCoinsTotal: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of an ongoing deposit operation.
|
||||||
|
*/
|
||||||
|
export interface PendingDepositOperation {
|
||||||
|
type: PendingOperationType.Deposit;
|
||||||
|
lastError: TalerErrorDetails | undefined;
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
depositGroupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fields that are present in every pending operation.
|
* Fields that are present in every pending operation.
|
||||||
*/
|
*/
|
||||||
|
@ -484,7 +484,7 @@ export class ContractTerms {
|
|||||||
/**
|
/**
|
||||||
* Extra data, interpreted by the mechant only.
|
* Extra data, interpreted by the mechant only.
|
||||||
*/
|
*/
|
||||||
extra: any;
|
extra?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,7 +94,8 @@ export type Transaction =
|
|||||||
| TransactionPayment
|
| TransactionPayment
|
||||||
| TransactionRefund
|
| TransactionRefund
|
||||||
| TransactionTip
|
| TransactionTip
|
||||||
| TransactionRefresh;
|
| TransactionRefresh
|
||||||
|
| TransactionDeposit;
|
||||||
|
|
||||||
export enum TransactionType {
|
export enum TransactionType {
|
||||||
Withdrawal = "withdrawal",
|
Withdrawal = "withdrawal",
|
||||||
@ -102,6 +103,7 @@ export enum TransactionType {
|
|||||||
Refund = "refund",
|
Refund = "refund",
|
||||||
Refresh = "refresh",
|
Refresh = "refresh",
|
||||||
Tip = "tip",
|
Tip = "tip",
|
||||||
|
Deposit = "deposit",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum WithdrawalType {
|
export enum WithdrawalType {
|
||||||
@ -308,6 +310,31 @@ interface TransactionRefresh extends TransactionCommon {
|
|||||||
amountEffective: AmountString;
|
amountEffective: AmountString;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deposit transaction, which effectively sends
|
||||||
|
* money from this wallet somewhere else.
|
||||||
|
*/
|
||||||
|
interface TransactionDeposit extends TransactionCommon {
|
||||||
|
type: TransactionType.Deposit;
|
||||||
|
|
||||||
|
depositGroupId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Target for the deposit.
|
||||||
|
*/
|
||||||
|
targetPaytoUri: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raw amount that is being deposited
|
||||||
|
*/
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Effective amount that is being deposited
|
||||||
|
*/
|
||||||
|
amountEffective: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
|
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
|
||||||
buildCodecForObject<TransactionsRequest>()
|
buildCodecForObject<TransactionsRequest>()
|
||||||
.property("currency", codecOptional(codecForString()))
|
.property("currency", codecOptional(codecForString()))
|
||||||
|
@ -1006,3 +1006,38 @@ export const codecForAbortPayWithRefundRequest = (): Codec<
|
|||||||
buildCodecForObject<AbortPayWithRefundRequest>()
|
buildCodecForObject<AbortPayWithRefundRequest>()
|
||||||
.property("proposalId", codecForString())
|
.property("proposalId", codecForString())
|
||||||
.build("AbortPayWithRefundRequest");
|
.build("AbortPayWithRefundRequest");
|
||||||
|
|
||||||
|
export interface CreateDepositGroupRequest {
|
||||||
|
depositPaytoUri: string;
|
||||||
|
amount: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForCreateDepositGroupRequest = (): Codec<
|
||||||
|
CreateDepositGroupRequest
|
||||||
|
> =>
|
||||||
|
buildCodecForObject<CreateDepositGroupRequest>()
|
||||||
|
.property("amount", codecForAmountString())
|
||||||
|
.property("depositPaytoUri", codecForString())
|
||||||
|
.build("CreateDepositGroupRequest");
|
||||||
|
|
||||||
|
export interface CreateDepositGroupResponse {
|
||||||
|
depositGroupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrackDepositGroupRequest {
|
||||||
|
depositGroupId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TrackDepositGroupResponse {
|
||||||
|
responses: {
|
||||||
|
status: number;
|
||||||
|
body: any;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForTrackDepositGroupRequest = (): Codec<
|
||||||
|
TrackDepositGroupRequest
|
||||||
|
> =>
|
||||||
|
buildCodecForObject<TrackDepositGroupRequest>()
|
||||||
|
.property("depositGroupId", codecForAmountString())
|
||||||
|
.build("TrackDepositGroupRequest");
|
||||||
|
@ -53,6 +53,7 @@ import {
|
|||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
RefundState,
|
RefundState,
|
||||||
MetaStores,
|
MetaStores,
|
||||||
|
DepositGroupRecord,
|
||||||
} from "./types/dbTypes";
|
} from "./types/dbTypes";
|
||||||
import { CoinDumpJson, WithdrawUriInfoResponse } from "./types/talerTypes";
|
import { CoinDumpJson, WithdrawUriInfoResponse } from "./types/talerTypes";
|
||||||
import {
|
import {
|
||||||
@ -96,6 +97,12 @@ import {
|
|||||||
codecForAbortPayWithRefundRequest,
|
codecForAbortPayWithRefundRequest,
|
||||||
ApplyRefundResponse,
|
ApplyRefundResponse,
|
||||||
RecoveryLoadRequest,
|
RecoveryLoadRequest,
|
||||||
|
codecForCreateDepositGroupRequest,
|
||||||
|
CreateDepositGroupRequest,
|
||||||
|
CreateDepositGroupResponse,
|
||||||
|
codecForTrackDepositGroupRequest,
|
||||||
|
TrackDepositGroupRequest,
|
||||||
|
TrackDepositGroupResponse,
|
||||||
} from "./types/walletTypes";
|
} from "./types/walletTypes";
|
||||||
import { Logger } from "./util/logging";
|
import { Logger } from "./util/logging";
|
||||||
|
|
||||||
@ -173,6 +180,11 @@ import {
|
|||||||
BackupInfo,
|
BackupInfo,
|
||||||
loadBackupRecovery,
|
loadBackupRecovery,
|
||||||
} from "./operations/backup";
|
} from "./operations/backup";
|
||||||
|
import {
|
||||||
|
createDepositGroup,
|
||||||
|
processDepositGroup,
|
||||||
|
trackDepositGroup,
|
||||||
|
} from "./operations/deposits";
|
||||||
|
|
||||||
const builtinCurrencies: CurrencyRecord[] = [
|
const builtinCurrencies: CurrencyRecord[] = [
|
||||||
{
|
{
|
||||||
@ -299,6 +311,9 @@ export class Wallet {
|
|||||||
case PendingOperationType.ExchangeCheckRefresh:
|
case PendingOperationType.ExchangeCheckRefresh:
|
||||||
await autoRefresh(this.ws, pending.exchangeBaseUrl);
|
await autoRefresh(this.ws, pending.exchangeBaseUrl);
|
||||||
break;
|
break;
|
||||||
|
case PendingOperationType.Deposit:
|
||||||
|
await processDepositGroup(this.ws, pending.depositGroupId);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
assertUnreachable(pending);
|
assertUnreachable(pending);
|
||||||
}
|
}
|
||||||
@ -972,6 +987,12 @@ export class Wallet {
|
|||||||
return addBackupProvider(this.ws, req);
|
return addBackupProvider(this.ws, req);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createDepositGroup(
|
||||||
|
req: CreateDepositGroupRequest,
|
||||||
|
): Promise<CreateDepositGroupResponse> {
|
||||||
|
return createDepositGroup(this.ws, req);
|
||||||
|
}
|
||||||
|
|
||||||
async runBackupCycle(): Promise<void> {
|
async runBackupCycle(): Promise<void> {
|
||||||
return runBackupCycle(this.ws);
|
return runBackupCycle(this.ws);
|
||||||
}
|
}
|
||||||
@ -980,6 +1001,12 @@ export class Wallet {
|
|||||||
return getBackupInfo(this.ws);
|
return getBackupInfo(this.ws);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async trackDepositGroup(
|
||||||
|
req: TrackDepositGroupRequest,
|
||||||
|
): Promise<TrackDepositGroupResponse> {
|
||||||
|
return trackDepositGroup(this.ws, req);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation of the "wallet-core" API.
|
* Implementation of the "wallet-core" API.
|
||||||
*/
|
*/
|
||||||
@ -1141,6 +1168,13 @@ export class Wallet {
|
|||||||
await runBackupCycle(this.ws);
|
await runBackupCycle(this.ws);
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
case "createDepositGroup": {
|
||||||
|
const req = codecForCreateDepositGroupRequest().decode(payload);
|
||||||
|
return await createDepositGroup(this.ws, req);
|
||||||
|
}
|
||||||
|
case "trackDepositGroup":
|
||||||
|
const req = codecForTrackDepositGroupRequest().decode(payload);
|
||||||
|
return trackDepositGroup(this.ws, req);
|
||||||
}
|
}
|
||||||
throw OperationFailedError.fromCode(
|
throw OperationFailedError.fromCode(
|
||||||
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
TalerErrorCode.WALLET_CORE_API_OPERATION_UNKNOWN,
|
||||||
|
@ -457,6 +457,18 @@ function TransactionItem(props: { tx: Transaction }): JSX.Element {
|
|||||||
pending={tx.pending}
|
pending={tx.pending}
|
||||||
></TransactionLayout>
|
></TransactionLayout>
|
||||||
);
|
);
|
||||||
|
case TransactionType.Deposit:
|
||||||
|
return (
|
||||||
|
<TransactionLayout
|
||||||
|
amount={tx.amountEffective}
|
||||||
|
debitCreditIndicator={"debit"}
|
||||||
|
title="Refresh"
|
||||||
|
subtitle={`to ${tx.targetPaytoUri}`}
|
||||||
|
timestamp={tx.timestamp}
|
||||||
|
iconPath="/static/img/ri-refresh-line.svg"
|
||||||
|
pending={tx.pending}
|
||||||
|
></TransactionLayout>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user