wallet-core: further towards deposit DD37

This commit is contained in:
Florian Dold 2023-04-23 22:49:42 +02:00
parent 321252040e
commit eff3920bd5
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
8 changed files with 521 additions and 191 deletions

View File

@ -267,7 +267,7 @@ deploymentCli
}); });
deploymentCli deploymentCli
.subcommand("testTalerdotnetDemo", "test-demo-talerdotnet") .subcommand("testTalerdotnetDemo", "test-demodottalerdotnet")
.action(async (args) => { .action(async (args) => {
const http = createPlatformHttpLib(); const http = createPlatformHttpLib();
const cryptiDisp = new CryptoDispatcher( const cryptiDisp = new CryptoDispatcher(
@ -295,6 +295,35 @@ deploymentCli
console.log("reserve status", reserveStatusResp.status); console.log("reserve status", reserveStatusResp.status);
}); });
deploymentCli
.subcommand("testDemoTestdotdalerdotnet", "test-testdottalerdotnet")
.action(async (args) => {
const http = createPlatformHttpLib();
const cryptiDisp = new CryptoDispatcher(
new SynchronousCryptoWorkerFactoryPlain(),
);
const cryptoApi = cryptiDisp.cryptoApi;
const reserveKeyPair = await cryptoApi.createEddsaKeypair({});
const exchangeBaseUrl = "https://exchange.test.taler.net/";
const exchangeInfo = await downloadExchangeInfo(exchangeBaseUrl, http);
await topupReserveWithDemobank({
amount: "TESTKUDOS:10",
bankAccessApiBaseUrl:
"https://bank.test.taler.net/demobanks/default/access-api/",
exchangeInfo,
http,
reservePub: reserveKeyPair.pub,
});
let reserveUrl = new URL(`reserves/${reserveKeyPair.pub}`, exchangeBaseUrl);
reserveUrl.searchParams.set("timeout_ms", "30000");
console.log("requesting", reserveUrl.href);
const longpollReq = http.fetch(reserveUrl.href, {
method: "GET",
});
const reserveStatusResp = await longpollReq;
console.log("reserve status", reserveStatusResp.status);
});
deploymentCli deploymentCli
.subcommand("testLocalhostDemo", "test-demo-localhost") .subcommand("testLocalhostDemo", "test-demo-localhost")
.action(async (args) => { .action(async (args) => {

View File

@ -868,6 +868,21 @@ export function bufferForUint32(n: number): Uint8Array {
return buf; return buf;
} }
/**
* This makes the assumption that the uint64 fits a float,
* which should be true for all Taler protocol messages.
*/
export function bufferForUint64(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(4);
const buf = new Uint8Array(arrBuf);
const dv = new DataView(arrBuf);
if (n < 0 || !Number.isInteger(n)) {
throw Error("non-negative integer expected");
}
dv.setBigUint64(0, BigInt(n));
return buf;
}
export function bufferForUint8(n: number): Uint8Array { export function bufferForUint8(n: number): Uint8Array {
const arrBuf = new ArrayBuffer(1); const arrBuf = new ArrayBuffer(1);
const buf = new Uint8Array(arrBuf); const buf = new Uint8Array(arrBuf);
@ -933,6 +948,7 @@ export enum TalerSignaturePurpose {
TEST = 4242, TEST = 4242,
MERCHANT_PAYMENT_OK = 1104, MERCHANT_PAYMENT_OK = 1104,
MERCHANT_CONTRACT = 1101, MERCHANT_CONTRACT = 1101,
MERCHANT_REFUND = 1102,
WALLET_COIN_RECOUP = 1203, WALLET_COIN_RECOUP = 1203,
WALLET_COIN_LINK = 1204, WALLET_COIN_LINK = 1204,
WALLET_COIN_RECOUP_REFRESH = 1206, WALLET_COIN_RECOUP_REFRESH = 1206,

View File

@ -386,8 +386,12 @@ walletCli
const transactionsCli = walletCli const transactionsCli = walletCli
.subcommand("transactions", "transactions", { help: "Manage transactions." }) .subcommand("transactions", "transactions", { help: "Manage transactions." })
.maybeOption("currency", ["--currency"], clk.STRING) .maybeOption("currency", ["--currency"], clk.STRING, {
.maybeOption("search", ["--search"], clk.STRING) help: "Filter by currency.",
})
.maybeOption("search", ["--search"], clk.STRING, {
help: "Filter by search string",
})
.flag("includeRefreshes", ["--include-refreshes"]); .flag("includeRefreshes", ["--include-refreshes"]);
// Default action // Default action
@ -420,6 +424,36 @@ transactionsCli
}); });
}); });
transactionsCli
.subcommand("suspendTransaction", "suspend", {
help: "Suspend a transaction.",
})
.requiredArgument("transactionId", clk.STRING, {
help: "Identifier of the transaction to suspend.",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.SuspendTransaction, {
transactionId: args.suspendTransaction.transactionId,
});
});
});
transactionsCli
.subcommand("resumeTransaction", "resume", {
help: "Resume a transaction.",
})
.requiredArgument("transactionId", clk.STRING, {
help: "Identifier of the transaction to suspend.",
})
.action(async (args) => {
await withWallet(args, async (wallet) => {
await wallet.client.call(WalletApiOperation.ResumeTransaction, {
transactionId: args.resumeTransaction.transactionId,
});
});
});
transactionsCli transactionsCli
.subcommand("lookup", "lookup", { .subcommand("lookup", "lookup", {
help: "Look up a single transaction based on the transaction identifier.", help: "Look up a single transaction based on the transaction identifier.",

View File

@ -33,6 +33,7 @@ import {
AmountString, AmountString,
BlindedDenominationSignature, BlindedDenominationSignature,
bufferForUint32, bufferForUint32,
bufferForUint64,
buildSigPS, buildSigPS,
CoinDepositPermission, CoinDepositPermission,
CoinEnvelope, CoinEnvelope,
@ -105,6 +106,8 @@ import {
EncryptedContract, EncryptedContract,
SignPurseMergeRequest, SignPurseMergeRequest,
SignPurseMergeResponse, SignPurseMergeResponse,
SignRefundRequest,
SignRefundResponse,
SignReservePurseCreateRequest, SignReservePurseCreateRequest,
SignReservePurseCreateResponse, SignReservePurseCreateResponse,
SignTrackTransactionRequest, SignTrackTransactionRequest,
@ -233,6 +236,8 @@ export interface TalerCryptoInterface {
signReservePurseCreate( signReservePurseCreate(
req: SignReservePurseCreateRequest, req: SignReservePurseCreateRequest,
): Promise<SignReservePurseCreateResponse>; ): Promise<SignReservePurseCreateResponse>;
signRefund(req: SignRefundRequest): Promise<SignRefundResponse>;
} }
/** /**
@ -409,6 +414,9 @@ export const nullCrypto: TalerCryptoInterface = {
): Promise<SignReservePurseCreateResponse> { ): Promise<SignReservePurseCreateResponse> {
throw new Error("Function not implemented."); throw new Error("Function not implemented.");
}, },
signRefund: function (req: SignRefundRequest): Promise<SignRefundResponse> {
throw new Error("Function not implemented.");
},
}; };
export type WithArg<X> = X extends (req: infer T) => infer R export type WithArg<X> = X extends (req: infer T) => infer R
@ -928,6 +936,7 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
const pub = decodeCrock(masterPub); const pub = decodeCrock(masterPub);
return { valid: eddsaVerify(p, sig, pub) }; return { valid: eddsaVerify(p, sig, pub) };
}, },
/** /**
* Check if the signature of a denomination is valid. * Check if the signature of a denomination is valid.
*/ */
@ -1625,6 +1634,24 @@ export const nativeCryptoR: TalerCryptoInterfaceR = {
purseSig: purseSigResp.sig, purseSig: purseSigResp.sig,
}; };
}, },
async signRefund(
tci: TalerCryptoInterfaceR,
req: SignRefundRequest,
): Promise<SignRefundResponse> {
const refundSigBlob = buildSigPS(TalerSignaturePurpose.MERCHANT_REFUND)
.put(decodeCrock(req.contractTermsHash))
.put(decodeCrock(req.coinPub))
.put(bufferForUint64(req.rtransactionId))
.put(amountToBuffer(req.refundAmount))
.build();
const refundSigResp = await tci.eddsaSign(tci, {
msg: encodeCrock(refundSigBlob),
priv: req.merchantPriv,
});
return {
sig: refundSigResp.sig,
};
},
}; };
function amountToBuffer(amount: AmountLike): Uint8Array { function amountToBuffer(amount: AmountLike): Uint8Array {

View File

@ -255,6 +255,21 @@ export interface SignPurseMergeResponse {
accountSig: string; accountSig: string;
} }
export interface SignRefundRequest {
merchantPriv: string;
merchantPub: string;
contractTermsHash: string;
coinPub: string;
rtransactionId: number;
refundAmount: AmountString;
}
export interface SignRefundResponse {
sig: string;
}
export interface SignRefundResponse {}
export interface SignReservePurseCreateRequest { export interface SignReservePurseCreateRequest {
mergeTimestamp: TalerProtocolTimestamp; mergeTimestamp: TalerProtocolTimestamp;

View File

@ -873,6 +873,8 @@ export enum DepositElementStatus {
Accepted = 20, Accepted = 20,
KycRequired = 30, KycRequired = 30,
Wired = 40, Wired = 40,
RefundSuccess = 50,
RefundFailed = 51,
} }
/** /**
@ -1639,6 +1641,14 @@ export interface BackupProviderRecord {
uids: string[]; uids: string[];
} }
export enum DepositOperationStatus {
Finished = 50 /* OperationStatusRange.DORMANT_START */,
Suspended = 51 /* OperationStatusRange.DORMANT_START + 1 */,
Aborted = 52 /* OperationStatusRange.DORMANT_START + 2 */,
Pending = 10 /* OperationStatusRange.ACTIVE_START */,
Aborting = 11 /* OperationStatusRange.ACTIVE_START + 1 */,
}
/** /**
* Group of deposits made by the wallet. * Group of deposits made by the wallet.
*/ */
@ -1680,16 +1690,26 @@ export interface DepositGroupRecord {
*/ */
effectiveDepositAmount: AmountString; effectiveDepositAmount: AmountString;
depositedPerCoin: boolean[];
timestampCreated: TalerProtocolTimestamp; timestampCreated: TalerProtocolTimestamp;
timestampFinished: TalerProtocolTimestamp | undefined; timestampFinished: TalerProtocolTimestamp | undefined;
operationStatus: OperationStatus; operationStatus: DepositOperationStatus;
// FIXME: Duplication between this and transactionPerCoin!
depositedPerCoin: boolean[];
// FIXME: Improve name!
transactionPerCoin: DepositElementStatus[]; transactionPerCoin: DepositElementStatus[];
/**
* When the deposit transaction was aborted and
* refreshes were tried, we create a refresh
* group and store the ID here.
*/
abortRefreshGroupId?: string;
// FIXME: Do we need this and should it be in this object store?
trackingState?: { trackingState?: {
[signature: string]: { [signature: string]: {
// Raw wire transfer identifier of the deposit. // Raw wire transfer identifier of the deposit.

View File

@ -64,13 +64,16 @@ import {
DepositElementStatus, DepositElementStatus,
} from "../db.js"; } from "../db.js";
import { TalerError } from "@gnu-taler/taler-util"; import { TalerError } from "@gnu-taler/taler-util";
import { getTotalRefreshCost, KycPendingInfo, KycUserType } from "../index.js"; import {
DepositOperationStatus,
getTotalRefreshCost,
KycPendingInfo,
KycUserType,
PendingTaskType,
} from "../index.js";
import { InternalWalletState } from "../internal-wallet-state.js"; import { InternalWalletState } from "../internal-wallet-state.js";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http"; import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import { import { OperationAttemptResult } from "../util/retries.js";
OperationAttemptResult,
OperationAttemptResultType,
} from "../util/retries.js";
import { spendCoins } from "./common.js"; import { spendCoins } from "./common.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { import {
@ -82,7 +85,9 @@ import { selectPayCoinsNew } from "../util/coinSelection.js";
import { import {
constructTransactionIdentifier, constructTransactionIdentifier,
parseTransactionIdentifier, parseTransactionIdentifier,
stopLongpolling,
} from "./transactions.js"; } from "./transactions.js";
import { constructTaskIdentifier } from "../util/retries.js";
/** /**
* Logger. * Logger.
@ -97,12 +102,12 @@ export function computeDepositTransactionStatus(
dg: DepositGroupRecord, dg: DepositGroupRecord,
): TransactionState { ): TransactionState {
switch (dg.operationStatus) { switch (dg.operationStatus) {
case OperationStatus.Finished: { case DepositOperationStatus.Finished: {
return { return {
major: TransactionMajorState.Done, major: TransactionMajorState.Done,
}; };
} }
case OperationStatus.Pending: { case DepositOperationStatus.Pending: {
const numTotal = dg.payCoinSelection.coinPubs.length; const numTotal = dg.payCoinSelection.coinPubs.length;
let numDeposited = 0; let numDeposited = 0;
let numKycRequired = 0; let numKycRequired = 0;
@ -140,6 +145,10 @@ export function computeDepositTransactionStatus(
minor: TransactionMinorState.Deposit, minor: TransactionMinorState.Deposit,
}; };
} }
case DepositOperationStatus.Suspended:
return {
major: TransactionMajorState.Suspended,
};
default: default:
throw Error("unexpected deposit group state"); throw Error("unexpected deposit group state");
} }
@ -149,13 +158,156 @@ export async function suspendDepositGroup(
ws: InternalWalletState, ws: InternalWalletState,
depositGroupId: string, depositGroupId: string,
): Promise<void> { ): Promise<void> {
throw Error("not implemented"); const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId,
});
const retryTag = constructTaskIdentifier({
tag: PendingTaskType.Deposit,
depositGroupId,
});
let res = await ws.db
.mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
logger.warn(
`can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
);
return undefined;
}
const oldState = computeDepositTransactionStatus(dg);
switch (dg.operationStatus) {
case DepositOperationStatus.Finished:
return undefined;
case DepositOperationStatus.Pending: {
dg.operationStatus = DepositOperationStatus.Suspended;
await tx.depositGroups.put(dg);
return {
oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg),
};
}
case DepositOperationStatus.Suspended:
return undefined;
}
return undefined;
});
stopLongpolling(ws, retryTag);
if (res) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: res.oldTxState,
newTxState: res.newTxState,
});
}
}
export async function resumeDepositGroup(
ws: InternalWalletState,
depositGroupId: string,
): Promise<void> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId,
});
let res = await ws.db
.mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
logger.warn(
`can't resume deposit group, depositGroupId=${depositGroupId} not found`,
);
return;
}
const oldState = computeDepositTransactionStatus(dg);
switch (dg.operationStatus) {
case DepositOperationStatus.Finished:
return;
case DepositOperationStatus.Pending: {
return;
}
case DepositOperationStatus.Suspended:
dg.operationStatus = DepositOperationStatus.Pending;
await tx.depositGroups.put(dg);
return {
oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg),
};
}
return undefined;
});
ws.latch.trigger();
if (res) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: res.oldTxState,
newTxState: res.newTxState,
});
}
} }
export async function abortDepositGroup( export async function abortDepositGroup(
ws: InternalWalletState, ws: InternalWalletState,
depositGroupId: string, depositGroupId: string,
): Promise<void> { ): Promise<void> {
const transactionId = constructTransactionIdentifier({
tag: TransactionType.Deposit,
depositGroupId,
});
const retryTag = constructTaskIdentifier({
tag: PendingTaskType.Deposit,
depositGroupId,
});
let res = await ws.db
.mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
logger.warn(
`can't suspend deposit group, depositGroupId=${depositGroupId} not found`,
);
return undefined;
}
const oldState = computeDepositTransactionStatus(dg);
switch (dg.operationStatus) {
case DepositOperationStatus.Finished:
return undefined;
case DepositOperationStatus.Pending: {
dg.operationStatus = DepositOperationStatus.Aborting;
await tx.depositGroups.put(dg);
return {
oldTxState: oldState,
newTxState: computeDepositTransactionStatus(dg),
};
}
case DepositOperationStatus.Suspended:
// FIXME: Can we abort a suspended transaction?!
return undefined;
}
return undefined;
});
stopLongpolling(ws, retryTag);
// Need to process the operation again.
ws.latch.trigger();
if (res) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: res.oldTxState,
newTxState: res.newTxState,
});
}
}
export async function deleteDepositGroup(
ws: InternalWalletState,
depositGroupId: boolean,
opts: { forced?: boolean } = {},
) {
throw Error("not implemented"); throw Error("not implemented");
} }
@ -230,195 +382,210 @@ export async function processDepositGroup(
const txStateOld = computeDepositTransactionStatus(depositGroup); const txStateOld = computeDepositTransactionStatus(depositGroup);
const contractData = extractContractData( if (depositGroup.operationStatus === DepositOperationStatus.Pending) {
depositGroup.contractTermsRaw, const contractData = extractContractData(
depositGroup.contractTermsHash, depositGroup.contractTermsRaw,
"", depositGroup.contractTermsHash,
); "",
);
// Check for cancellation before expensive operations. // Check for cancellation before expensive operations.
options.cancellationToken?.throwIfCancelled(); options.cancellationToken?.throwIfCancelled();
// FIXME: Cache these! // FIXME: Cache these!
const depositPermissions = await generateDepositPermissions( const depositPermissions = await generateDepositPermissions(
ws, ws,
depositGroup.payCoinSelection, depositGroup.payCoinSelection,
contractData, contractData,
); );
for (let i = 0; i < depositPermissions.length; i++) { for (let i = 0; i < depositPermissions.length; i++) {
const perm = depositPermissions[i]; const perm = depositPermissions[i];
let updatedDeposit: boolean = false; let updatedDeposit: boolean = false;
if (!depositGroup.depositedPerCoin[i]) { if (!depositGroup.depositedPerCoin[i]) {
const requestBody: ExchangeDepositRequest = { const requestBody: ExchangeDepositRequest = {
contribution: Amounts.stringify(perm.contribution), contribution: Amounts.stringify(perm.contribution),
merchant_payto_uri: depositGroup.wire.payto_uri, merchant_payto_uri: depositGroup.wire.payto_uri,
wire_salt: depositGroup.wire.salt, wire_salt: depositGroup.wire.salt,
h_contract_terms: depositGroup.contractTermsHash, h_contract_terms: depositGroup.contractTermsHash,
ub_sig: perm.ub_sig, ub_sig: perm.ub_sig,
timestamp: depositGroup.contractTermsRaw.timestamp, timestamp: depositGroup.contractTermsRaw.timestamp,
wire_transfer_deadline: wire_transfer_deadline:
depositGroup.contractTermsRaw.wire_transfer_deadline, depositGroup.contractTermsRaw.wire_transfer_deadline,
refund_deadline: depositGroup.contractTermsRaw.refund_deadline, refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
coin_sig: perm.coin_sig, coin_sig: perm.coin_sig,
denom_pub_hash: perm.h_denom, denom_pub_hash: perm.h_denom,
merchant_pub: depositGroup.merchantPub, merchant_pub: depositGroup.merchantPub,
h_age_commitment: perm.h_age_commitment, h_age_commitment: perm.h_age_commitment,
};
// Check for cancellation before making network request.
options.cancellationToken?.throwIfCancelled();
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
logger.info(`depositing to ${url}`);
const httpResp = await ws.http.fetch(url.href, {
method: "POST",
body: requestBody,
cancellationToken: options.cancellationToken,
});
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
updatedDeposit = true;
}
let updatedTxStatus: DepositElementStatus | undefined = undefined;
type ValueOf<T> = T[keyof T];
let newWiredTransaction:
| {
id: string;
value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
}
| undefined;
if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
const track = await trackDeposit(ws, depositGroup, perm);
if (track.type === "accepted") {
if (!track.kyc_ok && track.requirement_row !== undefined) {
updatedTxStatus = DepositElementStatus.KycRequired;
const { requirement_row: requirementRow } = track;
const paytoHash = encodeCrock(
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
);
await checkDepositKycStatus(
ws,
perm.exchange_url,
{ paytoHash, requirementRow },
"individual",
);
} else {
updatedTxStatus = DepositElementStatus.Accepted;
}
} else if (track.type === "wired") {
updatedTxStatus = DepositElementStatus.Wired;
const payto = parsePaytoUri(depositGroup.wire.payto_uri);
if (!payto) {
throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
}
const fee = await getExchangeWireFee(
ws,
payto.targetType,
perm.exchange_url,
track.execution_time,
);
const raw = Amounts.parseOrThrow(track.coin_contribution);
const wireFee = Amounts.parseOrThrow(fee.wireFee);
newWiredTransaction = {
value: {
amountRaw: Amounts.stringify(raw),
wireFee: Amounts.stringify(wireFee),
exchangePub: track.exchange_pub,
timestampExecuted: track.execution_time,
wireTransferId: track.wtid,
},
id: track.exchange_sig,
}; };
} else { // Check for cancellation before making network request.
updatedTxStatus = DepositElementStatus.Unknown; options.cancellationToken?.throwIfCancelled();
} const url = new URL(
} `coins/${perm.coin_pub}/deposit`,
perm.exchange_url,
if (updatedTxStatus !== undefined || updatedDeposit) { );
await ws.db logger.info(`depositing to ${url}`);
.mktx((x) => [x.depositGroups]) const httpResp = await ws.http.fetch(url.href, {
.runReadWrite(async (tx) => { method: "POST",
const dg = await tx.depositGroups.get(depositGroupId); body: requestBody,
if (!dg) { cancellationToken: options.cancellationToken,
return;
}
if (updatedDeposit !== undefined) {
dg.depositedPerCoin[i] = updatedDeposit;
}
if (updatedTxStatus !== undefined) {
dg.transactionPerCoin[i] = updatedTxStatus;
}
if (newWiredTransaction) {
if (!dg.trackingState) {
dg.trackingState = {};
}
dg.trackingState[newWiredTransaction.id] =
newWiredTransaction.value;
}
await tx.depositGroups.put(dg);
}); });
} await readSuccessResponseJsonOrThrow(
} httpResp,
codecForDepositSuccess(),
const txStatusNew = await ws.db );
.mktx((x) => [x.depositGroups]) updatedDeposit = true;
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
return undefined;
} }
let allDepositedAndWired = true;
for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) { let updatedTxStatus: DepositElementStatus | undefined = undefined;
if ( type ValueOf<T> = T[keyof T];
!depositGroup.depositedPerCoin[i] ||
depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired let newWiredTransaction:
) { | {
allDepositedAndWired = false; id: string;
break; value: ValueOf<NonNullable<DepositGroupRecord["trackingState"]>>;
}
| undefined;
if (depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired) {
const track = await trackDeposit(ws, depositGroup, perm);
if (track.type === "accepted") {
if (!track.kyc_ok && track.requirement_row !== undefined) {
updatedTxStatus = DepositElementStatus.KycRequired;
const { requirement_row: requirementRow } = track;
const paytoHash = encodeCrock(
hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
);
await checkDepositKycStatus(
ws,
perm.exchange_url,
{ paytoHash, requirementRow },
"individual",
);
} else {
updatedTxStatus = DepositElementStatus.Accepted;
}
} else if (track.type === "wired") {
updatedTxStatus = DepositElementStatus.Wired;
const payto = parsePaytoUri(depositGroup.wire.payto_uri);
if (!payto) {
throw Error(`unparsable payto: ${depositGroup.wire.payto_uri}`);
}
const fee = await getExchangeWireFee(
ws,
payto.targetType,
perm.exchange_url,
track.execution_time,
);
const raw = Amounts.parseOrThrow(track.coin_contribution);
const wireFee = Amounts.parseOrThrow(fee.wireFee);
newWiredTransaction = {
value: {
amountRaw: Amounts.stringify(raw),
wireFee: Amounts.stringify(wireFee),
exchangePub: track.exchange_pub,
timestampExecuted: track.execution_time,
wireTransferId: track.wtid,
},
id: track.exchange_sig,
};
} else {
updatedTxStatus = DepositElementStatus.Unknown;
} }
} }
if (allDepositedAndWired) {
dg.timestampFinished = TalerProtocolTimestamp.now(); if (updatedTxStatus !== undefined || updatedDeposit) {
dg.operationStatus = OperationStatus.Finished; await ws.db
await tx.depositGroups.put(dg); .mktx((x) => [x.depositGroups])
.runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
return;
}
if (updatedDeposit !== undefined) {
dg.depositedPerCoin[i] = updatedDeposit;
}
if (updatedTxStatus !== undefined) {
dg.transactionPerCoin[i] = updatedTxStatus;
}
if (newWiredTransaction) {
if (!dg.trackingState) {
dg.trackingState = {};
}
dg.trackingState[newWiredTransaction.id] =
newWiredTransaction.value;
}
await tx.depositGroups.put(dg);
});
} }
return computeDepositTransactionStatus(dg); }
});
if (!txStatusNew) { const txStatusNew = await ws.db
// Doesn't exist anymore! .mktx((x) => [x.depositGroups])
return OperationAttemptResult.finishedEmpty(); .runReadWrite(async (tx) => {
const dg = await tx.depositGroups.get(depositGroupId);
if (!dg) {
return undefined;
}
let allDepositedAndWired = true;
for (let i = 0; i < depositGroup.depositedPerCoin.length; i++) {
if (
!depositGroup.depositedPerCoin[i] ||
depositGroup.transactionPerCoin[i] !== DepositElementStatus.Wired
) {
allDepositedAndWired = false;
break;
}
}
if (allDepositedAndWired) {
dg.timestampFinished = TalerProtocolTimestamp.now();
dg.operationStatus = DepositOperationStatus.Finished;
await tx.depositGroups.put(dg);
}
return computeDepositTransactionStatus(dg);
});
if (!txStatusNew) {
// Doesn't exist anymore!
return OperationAttemptResult.finishedEmpty();
}
// Notify if state transitioned
if (
txStateOld.major !== txStatusNew.major ||
txStateOld.minor !== txStatusNew.minor
) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: txStateOld,
newTxState: txStatusNew,
});
}
// FIXME: consider other cases like aborting, suspend, ...
if (
txStatusNew.major === TransactionMajorState.Pending ||
txStatusNew.major === TransactionMajorState.Aborting
) {
return OperationAttemptResult.pendingEmpty();
} else {
return OperationAttemptResult.finishedEmpty();
}
} }
// Notify if state transitioned if (depositGroup.operationStatus === DepositOperationStatus.Aborting) {
if ( // FIXME: Implement!
txStateOld.major !== txStatusNew.major ||
txStateOld.minor !== txStatusNew.minor
) {
ws.notify({
type: NotificationType.TransactionStateTransition,
transactionId,
oldTxState: txStateOld,
newTxState: txStatusNew,
});
}
// FIXME: consider other cases like aborting, suspend, ...
if (
txStatusNew.major === TransactionMajorState.Pending ||
txStatusNew.major === TransactionMajorState.Aborting
) {
return OperationAttemptResult.pendingEmpty(); return OperationAttemptResult.pendingEmpty();
} else {
return OperationAttemptResult.finishedEmpty();
} }
return OperationAttemptResult.finishedEmpty();
} }
async function getExchangeWireFee( async function getExchangeWireFee(
@ -763,7 +930,7 @@ export async function createDepositGroup(
payto_uri: req.depositPaytoUri, payto_uri: req.depositPaytoUri,
salt: wireSalt, salt: wireSalt,
}, },
operationStatus: OperationStatus.Pending, operationStatus: DepositOperationStatus.Pending,
}; };
const transactionId = constructTransactionIdentifier({ const transactionId = constructTransactionIdentifier({

View File

@ -81,6 +81,7 @@ import {
import { import {
computeDepositTransactionStatus, computeDepositTransactionStatus,
processDepositGroup, processDepositGroup,
suspendDepositGroup,
} from "./deposits.js"; } from "./deposits.js";
import { getExchangeDetails } from "./exchanges.js"; import { getExchangeDetails } from "./exchanges.js";
import { import {
@ -1615,7 +1616,19 @@ export async function retryTransaction(
export async function suspendTransaction( export async function suspendTransaction(
ws: InternalWalletState, ws: InternalWalletState,
transactionId: string, transactionId: string,
): Promise<void> {} ): Promise<void> {
const tx = parseTransactionIdentifier(transactionId);
if (!tx) {
throw Error("invalid transaction ID");
}
switch (tx.tag) {
case TransactionType.Deposit:
await suspendDepositGroup(ws, tx.depositGroupId);
return;
default:
logger.warn(`unable to suspend transaction of type '${tx.tag}'`);
}
}
/** /**
* Resume a suspended transaction. * Resume a suspended transaction.
@ -1623,7 +1636,16 @@ export async function suspendTransaction(
export async function resumeTransaction( export async function resumeTransaction(
ws: InternalWalletState, ws: InternalWalletState,
transactionId: string, transactionId: string,
): Promise<void> {} ): Promise<void> {
const tx = parseTransactionIdentifier(transactionId);
if (!tx) {
throw Error("invalid transaction ID");
}
switch (tx.tag) {
default:
logger.warn(`unable to resume transaction of type '${tx.tag}'`);
}
}
/** /**
* Permanently delete a transaction based on the transaction ID. * Permanently delete a transaction based on the transaction ID.