wallet-core: further towards deposit DD37
This commit is contained in:
parent
321252040e
commit
eff3920bd5
@ -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) => {
|
||||||
|
@ -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,
|
||||||
|
@ -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.",
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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({
|
||||||
|
@ -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.
|
||||||
|
Loading…
Reference in New Issue
Block a user