implement peer to peer push payments
This commit is contained in:
parent
fb8372dfbf
commit
ac8f116780
@ -28,15 +28,14 @@ interface Props {
|
|||||||
mobile?: boolean;
|
mobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const VERSION: string = process.env.__VERSION__ || "dev";
|
// @ts-ignore
|
||||||
const GIT_HASH: string | undefined = process.env.__GIT_HASH__;
|
const maybeEnv = process?.env || {};
|
||||||
|
|
||||||
|
const VERSION: string = maybeEnv.__VERSION__ || "dev";
|
||||||
|
const GIT_HASH: string | undefined = maybeEnv.__GIT_HASH__;
|
||||||
const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` : VERSION;
|
const VERSION_WITH_HASH = GIT_HASH ? `${VERSION}-${GIT_HASH}` : VERSION;
|
||||||
|
|
||||||
export function Sidebar({ mobile }: Props): VNode {
|
export function Sidebar({ mobile }: Props): VNode {
|
||||||
// const config = useConfigContext();
|
|
||||||
const config = { version: "none" };
|
|
||||||
// FIXME: add replacement for __VERSION__ with the current version
|
|
||||||
const process = { env: { __VERSION__: "0.0.0" } };
|
|
||||||
const reducer = useAnastasisContext()!;
|
const reducer = useAnastasisContext()!;
|
||||||
|
|
||||||
function saveSession(): void {
|
function saveSession(): void {
|
||||||
|
@ -186,7 +186,7 @@ class UnionCodecBuilder<
|
|||||||
throw new DecodingError(
|
throw new DecodingError(
|
||||||
`expected tag for ${objectDisplayName} at ${renderContext(
|
`expected tag for ${objectDisplayName} at ${renderContext(
|
||||||
c,
|
c,
|
||||||
)}.${discriminator}`,
|
)}.${String(discriminator)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const alt = alternatives.get(d);
|
const alt = alternatives.get(d);
|
||||||
@ -194,7 +194,7 @@ class UnionCodecBuilder<
|
|||||||
throw new DecodingError(
|
throw new DecodingError(
|
||||||
`unknown tag for ${objectDisplayName} ${d} at ${renderContext(
|
`unknown tag for ${objectDisplayName} ${d} at ${renderContext(
|
||||||
c,
|
c,
|
||||||
)}.${discriminator}`,
|
)}.${String(discriminator)}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const altDecoded = alt.codec.decode(x);
|
const altDecoded = alt.codec.decode(x);
|
||||||
|
@ -18,8 +18,13 @@
|
|||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import test from "ava";
|
import test from "ava";
|
||||||
|
import { initNodePrng } from "./prng-node.js";
|
||||||
import { ContractTermsUtil } from "./contractTerms.js";
|
import { ContractTermsUtil } from "./contractTerms.js";
|
||||||
|
|
||||||
|
// Since we import nacl-fast directly (and not via index.node.ts), we need to
|
||||||
|
// init the PRNG manually.
|
||||||
|
initNodePrng();
|
||||||
|
|
||||||
test("contract terms canon hashing", (t) => {
|
test("contract terms canon hashing", (t) => {
|
||||||
const cReq = {
|
const cReq = {
|
||||||
foo: 42,
|
foo: 42,
|
||||||
|
@ -381,7 +381,7 @@ test("taler age restriction crypto", async (t) => {
|
|||||||
|
|
||||||
const pub2Ref = await Edx25519.getPublic(priv2);
|
const pub2Ref = await Edx25519.getPublic(priv2);
|
||||||
|
|
||||||
t.is(pub2, pub2Ref);
|
t.deepEqual(pub2, pub2Ref);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("edx signing", async (t) => {
|
test("edx signing", async (t) => {
|
||||||
@ -390,21 +390,13 @@ test("edx signing", async (t) => {
|
|||||||
|
|
||||||
const msg = stringToBytes("hello world");
|
const msg = stringToBytes("hello world");
|
||||||
|
|
||||||
const sig = nacl.crypto_edx25519_sign_detached(
|
const sig = nacl.crypto_edx25519_sign_detached(msg, priv1, pub1);
|
||||||
msg,
|
|
||||||
priv1,
|
|
||||||
pub1,
|
|
||||||
);
|
|
||||||
|
|
||||||
t.true(
|
t.true(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
|
||||||
nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1),
|
|
||||||
);
|
|
||||||
|
|
||||||
sig[0]++;
|
sig[0]++;
|
||||||
|
|
||||||
t.false(
|
t.false(nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1));
|
||||||
nacl.crypto_edx25519_sign_detached_verify(msg, sig, pub1),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("edx test vector", async (t) => {
|
test("edx test vector", async (t) => {
|
||||||
@ -422,18 +414,18 @@ test("edx test vector", async (t) => {
|
|||||||
|
|
||||||
{
|
{
|
||||||
const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx));
|
const pub1Prime = await Edx25519.getPublic(decodeCrock(tv.priv1_edx));
|
||||||
t.is(pub1Prime, decodeCrock(tv.pub1_edx));
|
t.deepEqual(pub1Prime, decodeCrock(tv.pub1_edx));
|
||||||
}
|
}
|
||||||
|
|
||||||
const pub2Prime = await Edx25519.publicKeyDerive(
|
const pub2Prime = await Edx25519.publicKeyDerive(
|
||||||
decodeCrock(tv.pub1_edx),
|
decodeCrock(tv.pub1_edx),
|
||||||
decodeCrock(tv.seed),
|
decodeCrock(tv.seed),
|
||||||
);
|
);
|
||||||
t.is(pub2Prime, decodeCrock(tv.pub2_edx));
|
t.deepEqual(pub2Prime, decodeCrock(tv.pub2_edx));
|
||||||
|
|
||||||
const priv2Prime = await Edx25519.privateKeyDerive(
|
const priv2Prime = await Edx25519.privateKeyDerive(
|
||||||
decodeCrock(tv.priv1_edx),
|
decodeCrock(tv.priv1_edx),
|
||||||
decodeCrock(tv.seed),
|
decodeCrock(tv.seed),
|
||||||
);
|
);
|
||||||
t.is(priv2Prime, decodeCrock(tv.priv2_edx));
|
t.deepEqual(priv2Prime, decodeCrock(tv.priv2_edx));
|
||||||
});
|
});
|
||||||
|
@ -20,6 +20,8 @@ import {
|
|||||||
parseWithdrawUri,
|
parseWithdrawUri,
|
||||||
parseRefundUri,
|
parseRefundUri,
|
||||||
parseTipUri,
|
parseTipUri,
|
||||||
|
parsePayPushUri,
|
||||||
|
constructPayPushUri,
|
||||||
} from "./taleruri.js";
|
} from "./taleruri.js";
|
||||||
|
|
||||||
test("taler pay url parsing: wrong scheme", (t) => {
|
test("taler pay url parsing: wrong scheme", (t) => {
|
||||||
@ -182,3 +184,44 @@ test("taler tip pickup uri with instance and prefix", (t) => {
|
|||||||
t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/");
|
t.is(r1.merchantBaseUrl, "https://merchant.example.com/my/pfx/tipm/");
|
||||||
t.is(r1.merchantTipId, "tipid");
|
t.is(r1.merchantTipId, "tipid");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("taler peer to peer push URI", (t) => {
|
||||||
|
const url1 = "taler://pay-push/exch.example.com/foo";
|
||||||
|
const r1 = parsePayPushUri(url1);
|
||||||
|
if (!r1) {
|
||||||
|
t.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.is(r1.exchangeBaseUrl, "https://exch.example.com/");
|
||||||
|
t.is(r1.contractPriv, "foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("taler peer to peer push URI (path)", (t) => {
|
||||||
|
const url1 = "taler://pay-push/exch.example.com:123/bla/foo";
|
||||||
|
const r1 = parsePayPushUri(url1);
|
||||||
|
if (!r1) {
|
||||||
|
t.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.is(r1.exchangeBaseUrl, "https://exch.example.com:123/bla/");
|
||||||
|
t.is(r1.contractPriv, "foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("taler peer to peer push URI (http)", (t) => {
|
||||||
|
const url1 = "taler+http://pay-push/exch.example.com:123/bla/foo";
|
||||||
|
const r1 = parsePayPushUri(url1);
|
||||||
|
if (!r1) {
|
||||||
|
t.fail();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
t.is(r1.exchangeBaseUrl, "http://exch.example.com:123/bla/");
|
||||||
|
t.is(r1.contractPriv, "foo");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("taler peer to peer push URI (construction)", (t) => {
|
||||||
|
const url = constructPayPushUri({
|
||||||
|
exchangeBaseUrl: "https://foo.example.com/bla/",
|
||||||
|
contractPriv: "123",
|
||||||
|
});
|
||||||
|
t.deepEqual(url, "taler://pay-push/foo.example.com/bla/123");
|
||||||
|
});
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { canonicalizeBaseUrl } from "./helpers.js";
|
import { canonicalizeBaseUrl } from "./helpers.js";
|
||||||
import { URLSearchParams } from "./url.js";
|
import { URLSearchParams, URL } from "./url.js";
|
||||||
|
|
||||||
export interface PayUriResult {
|
export interface PayUriResult {
|
||||||
merchantBaseUrl: string;
|
merchantBaseUrl: string;
|
||||||
@ -40,6 +40,11 @@ export interface TipUriResult {
|
|||||||
merchantBaseUrl: string;
|
merchantBaseUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PayPushUriResult {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
contractPriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a taler[+http]://withdraw URI.
|
* Parse a taler[+http]://withdraw URI.
|
||||||
* Return undefined if not passed a valid URI.
|
* Return undefined if not passed a valid URI.
|
||||||
@ -79,6 +84,7 @@ export enum TalerUriType {
|
|||||||
TalerTip = "taler-tip",
|
TalerTip = "taler-tip",
|
||||||
TalerRefund = "taler-refund",
|
TalerRefund = "taler-refund",
|
||||||
TalerNotifyReserve = "taler-notify-reserve",
|
TalerNotifyReserve = "taler-notify-reserve",
|
||||||
|
TalerPayPush = "pay-push",
|
||||||
Unknown = "unknown",
|
Unknown = "unknown",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,6 +117,12 @@ export function classifyTalerUri(s: string): TalerUriType {
|
|||||||
if (sl.startsWith("taler+http://withdraw/")) {
|
if (sl.startsWith("taler+http://withdraw/")) {
|
||||||
return TalerUriType.TalerWithdraw;
|
return TalerUriType.TalerWithdraw;
|
||||||
}
|
}
|
||||||
|
if (sl.startsWith("taler://pay-push/")) {
|
||||||
|
return TalerUriType.TalerPayPush;
|
||||||
|
}
|
||||||
|
if (sl.startsWith("taler+http://pay-push/")) {
|
||||||
|
return TalerUriType.TalerPayPush;
|
||||||
|
}
|
||||||
if (sl.startsWith("taler://notify-reserve/")) {
|
if (sl.startsWith("taler://notify-reserve/")) {
|
||||||
return TalerUriType.TalerNotifyReserve;
|
return TalerUriType.TalerNotifyReserve;
|
||||||
}
|
}
|
||||||
@ -176,6 +188,28 @@ export function parsePayUri(s: string): PayUriResult | undefined {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function parsePayPushUri(s: string): PayPushUriResult | undefined {
|
||||||
|
const pi = parseProtoInfo(s, "pay-push");
|
||||||
|
if (!pi) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const c = pi?.rest.split("?");
|
||||||
|
const parts = c[0].split("/");
|
||||||
|
if (parts.length < 2) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const host = parts[0].toLowerCase();
|
||||||
|
const contractPriv = parts[parts.length - 1];
|
||||||
|
const pathSegments = parts.slice(1, parts.length - 1);
|
||||||
|
const p = [host, ...pathSegments].join("/");
|
||||||
|
const exchangeBaseUrl = canonicalizeBaseUrl(`${pi.innerProto}://${p}/`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exchangeBaseUrl,
|
||||||
|
contractPriv,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a taler[+http]://tip URI.
|
* Parse a taler[+http]://tip URI.
|
||||||
* Return undefined if not passed a valid URI.
|
* Return undefined if not passed a valid URI.
|
||||||
@ -228,3 +262,24 @@ export function parseRefundUri(s: string): RefundUriResult | undefined {
|
|||||||
orderId,
|
orderId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function constructPayPushUri(args: {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
contractPriv: string;
|
||||||
|
}): string {
|
||||||
|
const url = new URL(args.exchangeBaseUrl);
|
||||||
|
let proto: string;
|
||||||
|
if (url.protocol === "https:") {
|
||||||
|
proto = "taler";
|
||||||
|
} else if (url.protocol === "http:") {
|
||||||
|
proto = "taler+http";
|
||||||
|
} else {
|
||||||
|
throw Error(`Unsupported exchange URL protocol ${args.exchangeBaseUrl}`);
|
||||||
|
}
|
||||||
|
if (!url.pathname.endsWith("/")) {
|
||||||
|
throw Error(
|
||||||
|
`exchange base URL must end with a slash (got ${args.exchangeBaseUrl}instead)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return `${proto}://pay-push/${url.host}${url.pathname}${args.contractPriv}`;
|
||||||
|
}
|
||||||
|
@ -92,6 +92,14 @@ export namespace Duration {
|
|||||||
return { d_ms: deadline.t_ms - now.t_ms };
|
return { d_ms: deadline.t_ms - now.t_ms };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function max(d1: Duration, d2: Duration): Duration {
|
||||||
|
return durationMax(d1, d2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function min(d1: Duration, d2: Duration): Duration {
|
||||||
|
return durationMin(d1, d2);
|
||||||
|
}
|
||||||
|
|
||||||
export function toIntegerYears(d: Duration): number {
|
export function toIntegerYears(d: Duration): number {
|
||||||
if (typeof d.d_ms !== "number") {
|
if (typeof d.d_ms !== "number") {
|
||||||
throw Error("infinite duration");
|
throw Error("infinite duration");
|
||||||
|
@ -858,10 +858,11 @@ interface GetContractTermsDetailsRequest {
|
|||||||
proposalId: string;
|
proposalId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForGetContractTermsDetails = (): Codec<GetContractTermsDetailsRequest> =>
|
export const codecForGetContractTermsDetails =
|
||||||
buildCodecForObject<GetContractTermsDetailsRequest>()
|
(): Codec<GetContractTermsDetailsRequest> =>
|
||||||
.property("proposalId", codecForString())
|
buildCodecForObject<GetContractTermsDetailsRequest>()
|
||||||
.build("GetContractTermsDetails");
|
.property("proposalId", codecForString())
|
||||||
|
.build("GetContractTermsDetails");
|
||||||
|
|
||||||
export interface PreparePayRequest {
|
export interface PreparePayRequest {
|
||||||
talerPayUri: string;
|
talerPayUri: string;
|
||||||
@ -1280,6 +1281,7 @@ export interface InitiatePeerPushPaymentResponse {
|
|||||||
pursePub: string;
|
pursePub: string;
|
||||||
mergePriv: string;
|
mergePriv: string;
|
||||||
contractPriv: string;
|
contractPriv: string;
|
||||||
|
talerUri: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForInitiatePeerPushPaymentRequest =
|
export const codecForInitiatePeerPushPaymentRequest =
|
||||||
@ -1290,32 +1292,30 @@ export const codecForInitiatePeerPushPaymentRequest =
|
|||||||
.build("InitiatePeerPushPaymentRequest");
|
.build("InitiatePeerPushPaymentRequest");
|
||||||
|
|
||||||
export interface CheckPeerPushPaymentRequest {
|
export interface CheckPeerPushPaymentRequest {
|
||||||
exchangeBaseUrl: string;
|
talerUri: string;
|
||||||
pursePub: string;
|
|
||||||
contractPriv: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CheckPeerPushPaymentResponse {
|
export interface CheckPeerPushPaymentResponse {
|
||||||
contractTerms: any;
|
contractTerms: any;
|
||||||
amount: AmountString;
|
amount: AmountString;
|
||||||
|
peerPushPaymentIncomingId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForCheckPeerPushPaymentRequest =
|
export const codecForCheckPeerPushPaymentRequest =
|
||||||
(): Codec<CheckPeerPushPaymentRequest> =>
|
(): Codec<CheckPeerPushPaymentRequest> =>
|
||||||
buildCodecForObject<CheckPeerPushPaymentRequest>()
|
buildCodecForObject<CheckPeerPushPaymentRequest>()
|
||||||
.property("pursePub", codecForString())
|
.property("talerUri", codecForString())
|
||||||
.property("contractPriv", codecForString())
|
|
||||||
.property("exchangeBaseUrl", codecForString())
|
|
||||||
.build("CheckPeerPushPaymentRequest");
|
.build("CheckPeerPushPaymentRequest");
|
||||||
|
|
||||||
export interface AcceptPeerPushPaymentRequest {
|
export interface AcceptPeerPushPaymentRequest {
|
||||||
exchangeBaseUrl: string;
|
/**
|
||||||
pursePub: string;
|
* Transparent identifier of the incoming peer push payment.
|
||||||
|
*/
|
||||||
|
peerPushPaymentIncomingId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const codecForAcceptPeerPushPaymentRequest =
|
export const codecForAcceptPeerPushPaymentRequest =
|
||||||
(): Codec<AcceptPeerPushPaymentRequest> =>
|
(): Codec<AcceptPeerPushPaymentRequest> =>
|
||||||
buildCodecForObject<AcceptPeerPushPaymentRequest>()
|
buildCodecForObject<AcceptPeerPushPaymentRequest>()
|
||||||
.property("pursePub", codecForString())
|
.property("peerPushPaymentIncomingId", codecForString())
|
||||||
.property("exchangeBaseUrl", codecForString())
|
|
||||||
.build("AcceptPeerPushPaymentRequest");
|
.build("AcceptPeerPushPaymentRequest");
|
||||||
|
@ -70,7 +70,7 @@ import {
|
|||||||
TipCreateConfirmation,
|
TipCreateConfirmation,
|
||||||
TipCreateRequest,
|
TipCreateRequest,
|
||||||
TippingReserveStatus,
|
TippingReserveStatus,
|
||||||
} from "./merchantApiTypes";
|
} from "./merchantApiTypes.js";
|
||||||
|
|
||||||
const exec = util.promisify(require("child_process").exec);
|
const exec = util.promisify(require("child_process").exec);
|
||||||
|
|
||||||
@ -478,14 +478,14 @@ class BankServiceBase {
|
|||||||
protected globalTestState: GlobalTestState,
|
protected globalTestState: GlobalTestState,
|
||||||
protected bankConfig: BankConfig,
|
protected bankConfig: BankConfig,
|
||||||
protected configFile: string,
|
protected configFile: string,
|
||||||
) { }
|
) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Work in progress. The key point is that both Sandbox and Nexus
|
* Work in progress. The key point is that both Sandbox and Nexus
|
||||||
* will be configured and started by this class.
|
* will be configured and started by this class.
|
||||||
*/
|
*/
|
||||||
class EufinBankService extends BankServiceBase implements BankServiceHandle {
|
class LibEuFinBankService extends BankServiceBase implements BankServiceHandle {
|
||||||
sandboxProc: ProcessWrapper | undefined;
|
sandboxProc: ProcessWrapper | undefined;
|
||||||
nexusProc: ProcessWrapper | undefined;
|
nexusProc: ProcessWrapper | undefined;
|
||||||
|
|
||||||
@ -494,8 +494,8 @@ class EufinBankService extends BankServiceBase implements BankServiceHandle {
|
|||||||
static async create(
|
static async create(
|
||||||
gc: GlobalTestState,
|
gc: GlobalTestState,
|
||||||
bc: BankConfig,
|
bc: BankConfig,
|
||||||
): Promise<EufinBankService> {
|
): Promise<LibEuFinBankService> {
|
||||||
return new EufinBankService(gc, bc, "foo");
|
return new LibEuFinBankService(gc, bc, "foo");
|
||||||
}
|
}
|
||||||
|
|
||||||
get port() {
|
get port() {
|
||||||
@ -761,7 +761,10 @@ class EufinBankService extends BankServiceBase implements BankServiceHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PybankService extends BankServiceBase implements BankServiceHandle {
|
/**
|
||||||
|
* Implementation of the bank service using the "taler-fakebank-run" tool.
|
||||||
|
*/
|
||||||
|
class FakebankService extends BankServiceBase implements BankServiceHandle {
|
||||||
proc: ProcessWrapper | undefined;
|
proc: ProcessWrapper | undefined;
|
||||||
|
|
||||||
http = new NodeHttpLib();
|
http = new NodeHttpLib();
|
||||||
@ -769,41 +772,23 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
|
|||||||
static async create(
|
static async create(
|
||||||
gc: GlobalTestState,
|
gc: GlobalTestState,
|
||||||
bc: BankConfig,
|
bc: BankConfig,
|
||||||
): Promise<PybankService> {
|
): Promise<FakebankService> {
|
||||||
const config = new Configuration();
|
const config = new Configuration();
|
||||||
setTalerPaths(config, gc.testDir + "/talerhome");
|
setTalerPaths(config, gc.testDir + "/talerhome");
|
||||||
config.setString("taler", "currency", bc.currency);
|
config.setString("taler", "currency", bc.currency);
|
||||||
config.setString("bank", "database", bc.database);
|
|
||||||
config.setString("bank", "http_port", `${bc.httpPort}`);
|
config.setString("bank", "http_port", `${bc.httpPort}`);
|
||||||
config.setString("bank", "serve", "http");
|
config.setString("bank", "serve", "http");
|
||||||
config.setString("bank", "max_debt_bank", `${bc.currency}:999999`);
|
config.setString("bank", "max_debt_bank", `${bc.currency}:999999`);
|
||||||
config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`);
|
config.setString("bank", "max_debt", bc.maxDebt ?? `${bc.currency}:100`);
|
||||||
config.setString(
|
|
||||||
"bank",
|
|
||||||
"allow_registrations",
|
|
||||||
bc.allowRegistrations ? "yes" : "no",
|
|
||||||
);
|
|
||||||
const cfgFilename = gc.testDir + "/bank.conf";
|
const cfgFilename = gc.testDir + "/bank.conf";
|
||||||
config.write(cfgFilename);
|
config.write(cfgFilename);
|
||||||
|
|
||||||
await sh(
|
return new FakebankService(gc, bc, cfgFilename);
|
||||||
gc,
|
|
||||||
"taler-bank-manage_django",
|
|
||||||
`taler-bank-manage -c '${cfgFilename}' django migrate`,
|
|
||||||
);
|
|
||||||
await sh(
|
|
||||||
gc,
|
|
||||||
"taler-bank-manage_django",
|
|
||||||
`taler-bank-manage -c '${cfgFilename}' django provide_accounts`,
|
|
||||||
);
|
|
||||||
|
|
||||||
return new PybankService(gc, bc, cfgFilename);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
|
setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
|
||||||
const config = Configuration.load(this.configFile);
|
const config = Configuration.load(this.configFile);
|
||||||
config.setString("bank", "suggested_exchange", e.baseUrl);
|
config.setString("bank", "suggested_exchange", e.baseUrl);
|
||||||
config.setString("bank", "suggested_exchange_payto", exchangePayto);
|
|
||||||
config.write(this.configFile);
|
config.write(this.configFile);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -815,21 +800,6 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
|
|||||||
accountName: string,
|
accountName: string,
|
||||||
password: string,
|
password: string,
|
||||||
): Promise<HarnessExchangeBankAccount> {
|
): Promise<HarnessExchangeBankAccount> {
|
||||||
await sh(
|
|
||||||
this.globalTestState,
|
|
||||||
"taler-bank-manage_django",
|
|
||||||
`taler-bank-manage -c '${this.configFile}' django add_bank_account ${accountName}`,
|
|
||||||
);
|
|
||||||
await sh(
|
|
||||||
this.globalTestState,
|
|
||||||
"taler-bank-manage_django",
|
|
||||||
`taler-bank-manage -c '${this.configFile}' django changepassword_unsafe ${accountName} ${password}`,
|
|
||||||
);
|
|
||||||
await sh(
|
|
||||||
this.globalTestState,
|
|
||||||
"taler-bank-manage_django",
|
|
||||||
`taler-bank-manage -c '${this.configFile}' django top_up ${accountName} ${this.bankConfig.currency}:100000`,
|
|
||||||
);
|
|
||||||
return {
|
return {
|
||||||
accountName: accountName,
|
accountName: accountName,
|
||||||
accountPassword: password,
|
accountPassword: password,
|
||||||
@ -844,8 +814,8 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
|
|||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.proc = this.globalTestState.spawnService(
|
this.proc = this.globalTestState.spawnService(
|
||||||
"taler-bank-manage",
|
"taler-fakebank-run",
|
||||||
["-c", this.configFile, "serve"],
|
["-c", this.configFile],
|
||||||
"bank",
|
"bank",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -857,7 +827,7 @@ class PybankService extends BankServiceBase implements BankServiceHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Use libeufin bank instead of pybank.
|
// Use libeufin bank instead of pybank.
|
||||||
const useLibeufinBank = process.env.WALLET_HARNESS_WITH_EUFIN;
|
const useLibeufinBank = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a euFin or a pyBank implementation of
|
* Return a euFin or a pyBank implementation of
|
||||||
@ -866,21 +836,21 @@ const useLibeufinBank = process.env.WALLET_HARNESS_WITH_EUFIN;
|
|||||||
* on a particular env variable.
|
* on a particular env variable.
|
||||||
*/
|
*/
|
||||||
function getBankServiceImpl(): {
|
function getBankServiceImpl(): {
|
||||||
prototype: typeof PybankService.prototype;
|
prototype: typeof FakebankService.prototype;
|
||||||
create: typeof PybankService.create;
|
create: typeof FakebankService.create;
|
||||||
} {
|
} {
|
||||||
if (useLibeufinBank)
|
if (useLibeufinBank)
|
||||||
return {
|
return {
|
||||||
prototype: EufinBankService.prototype,
|
prototype: LibEuFinBankService.prototype,
|
||||||
create: EufinBankService.create,
|
create: LibEuFinBankService.create,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
prototype: PybankService.prototype,
|
prototype: FakebankService.prototype,
|
||||||
create: PybankService.create,
|
create: FakebankService.create,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BankService = PybankService;
|
export type BankService = FakebankService;
|
||||||
export const BankService = getBankServiceImpl();
|
export const BankService = getBankServiceImpl();
|
||||||
|
|
||||||
export class FakeBankService {
|
export class FakeBankService {
|
||||||
@ -923,7 +893,7 @@ export class FakeBankService {
|
|||||||
private globalTestState: GlobalTestState,
|
private globalTestState: GlobalTestState,
|
||||||
private bankConfig: FakeBankConfig,
|
private bankConfig: FakeBankConfig,
|
||||||
private configFile: string,
|
private configFile: string,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
this.proc = this.globalTestState.spawnService(
|
this.proc = this.globalTestState.spawnService(
|
||||||
@ -1189,7 +1159,7 @@ export class ExchangeService implements ExchangeServiceInterface {
|
|||||||
private exchangeConfig: ExchangeConfig,
|
private exchangeConfig: ExchangeConfig,
|
||||||
private configFilename: string,
|
private configFilename: string,
|
||||||
private keyPair: EddsaKeyPair,
|
private keyPair: EddsaKeyPair,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return this.exchangeConfig.name;
|
return this.exchangeConfig.name;
|
||||||
@ -1442,7 +1412,7 @@ export class MerchantApiClient {
|
|||||||
constructor(
|
constructor(
|
||||||
private baseUrl: string,
|
private baseUrl: string,
|
||||||
public readonly auth: MerchantAuthConfiguration,
|
public readonly auth: MerchantAuthConfiguration,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
|
async changeAuth(auth: MerchantAuthConfiguration): Promise<void> {
|
||||||
const url = new URL("private/auth", this.baseUrl);
|
const url = new URL("private/auth", this.baseUrl);
|
||||||
@ -1635,7 +1605,7 @@ export class MerchantService implements MerchantServiceInterface {
|
|||||||
private globalState: GlobalTestState,
|
private globalState: GlobalTestState,
|
||||||
private merchantConfig: MerchantConfig,
|
private merchantConfig: MerchantConfig,
|
||||||
private configFilename: string,
|
private configFilename: string,
|
||||||
) { }
|
) {}
|
||||||
|
|
||||||
private currentTimetravel: Duration | undefined;
|
private currentTimetravel: Duration | undefined;
|
||||||
|
|
||||||
@ -1947,8 +1917,10 @@ export class WalletCli {
|
|||||||
const resp = await sh(
|
const resp = await sh(
|
||||||
self.globalTestState,
|
self.globalTestState,
|
||||||
`wallet-${self.name}`,
|
`wallet-${self.name}`,
|
||||||
`taler-wallet-cli ${self.timetravelArg ?? ""
|
`taler-wallet-cli ${
|
||||||
} --no-throttle -LTRACE --wallet-db '${self.dbfile
|
self.timetravelArg ?? ""
|
||||||
|
} --no-throttle -LTRACE --wallet-db '${
|
||||||
|
self.dbfile
|
||||||
}' api '${op}' ${shellWrap(JSON.stringify(payload))}`,
|
}' api '${op}' ${shellWrap(JSON.stringify(payload))}`,
|
||||||
);
|
);
|
||||||
console.log("--- wallet core response ---");
|
console.log("--- wallet core response ---");
|
||||||
|
@ -36,7 +36,7 @@ import {
|
|||||||
runCommand,
|
runCommand,
|
||||||
setupDb,
|
setupDb,
|
||||||
sh,
|
sh,
|
||||||
getRandomIban
|
getRandomIban,
|
||||||
} from "../harness/harness.js";
|
} from "../harness/harness.js";
|
||||||
import {
|
import {
|
||||||
LibeufinSandboxApi,
|
LibeufinSandboxApi,
|
||||||
@ -53,13 +53,10 @@ import {
|
|||||||
CreateAnastasisFacadeRequest,
|
CreateAnastasisFacadeRequest,
|
||||||
PostNexusTaskRequest,
|
PostNexusTaskRequest,
|
||||||
PostNexusPermissionRequest,
|
PostNexusPermissionRequest,
|
||||||
CreateNexusUserRequest
|
CreateNexusUserRequest,
|
||||||
} from "../harness/libeufin-apis.js";
|
} from "../harness/libeufin-apis.js";
|
||||||
|
|
||||||
export {
|
export { LibeufinSandboxApi, LibeufinNexusApi };
|
||||||
LibeufinSandboxApi,
|
|
||||||
LibeufinNexusApi
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LibeufinServices {
|
export interface LibeufinServices {
|
||||||
libeufinSandbox: LibeufinSandboxService;
|
libeufinSandbox: LibeufinSandboxService;
|
||||||
@ -206,6 +203,16 @@ export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async start(): Promise<void> {
|
async start(): Promise<void> {
|
||||||
|
await sh(
|
||||||
|
this.globalTestState,
|
||||||
|
"libeufin-sandbox-config",
|
||||||
|
"libeufin-sandbox config default",
|
||||||
|
{
|
||||||
|
...process.env,
|
||||||
|
LIBEUFIN_SANDBOX_DB_CONNECTION: this.sandboxConfig.databaseJdbcUri,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.sandboxProc = this.globalTestState.spawnService(
|
this.sandboxProc = this.globalTestState.spawnService(
|
||||||
"libeufin-sandbox",
|
"libeufin-sandbox",
|
||||||
["serve", "--port", `${this.sandboxConfig.httpPort}`],
|
["serve", "--port", `${this.sandboxConfig.httpPort}`],
|
||||||
@ -235,7 +242,8 @@ export class LibeufinSandboxService implements LibeufinSandboxServiceInterface {
|
|||||||
debit: string,
|
debit: string,
|
||||||
credit: string,
|
credit: string,
|
||||||
amount: string, // $currency:x.y
|
amount: string, // $currency:x.y
|
||||||
subject: string,): Promise<string> {
|
subject: string,
|
||||||
|
): Promise<string> {
|
||||||
const stdout = await sh(
|
const stdout = await sh(
|
||||||
this.globalTestState,
|
this.globalTestState,
|
||||||
"libeufin-sandbox-maketransfer",
|
"libeufin-sandbox-maketransfer",
|
||||||
@ -428,7 +436,7 @@ export class LibeufinCli {
|
|||||||
LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl,
|
LIBEUFIN_SANDBOX_URL: this.cliDetails.sandboxUrl,
|
||||||
LIBEUFIN_SANDBOX_USERNAME: "admin",
|
LIBEUFIN_SANDBOX_USERNAME: "admin",
|
||||||
LIBEUFIN_SANDBOX_PASSWORD: "secret",
|
LIBEUFIN_SANDBOX_PASSWORD: "secret",
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async checkSandbox(): Promise<void> {
|
async checkSandbox(): Promise<void> {
|
||||||
@ -436,7 +444,7 @@ export class LibeufinCli {
|
|||||||
this.globalTestState,
|
this.globalTestState,
|
||||||
"libeufin-cli-checksandbox",
|
"libeufin-cli-checksandbox",
|
||||||
"libeufin-cli sandbox check",
|
"libeufin-cli sandbox check",
|
||||||
this.env()
|
this.env(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -445,7 +453,7 @@ export class LibeufinCli {
|
|||||||
this.globalTestState,
|
this.globalTestState,
|
||||||
"libeufin-cli-createebicshost",
|
"libeufin-cli-createebicshost",
|
||||||
`libeufin-cli sandbox ebicshost create --host-id=${hostId}`,
|
`libeufin-cli sandbox ebicshost create --host-id=${hostId}`,
|
||||||
this.env()
|
this.env(),
|
||||||
);
|
);
|
||||||
console.log(stdout);
|
console.log(stdout);
|
||||||
}
|
}
|
||||||
@ -460,7 +468,7 @@ export class LibeufinCli {
|
|||||||
` --host-id=${details.hostId}` +
|
` --host-id=${details.hostId}` +
|
||||||
` --partner-id=${details.partnerId}` +
|
` --partner-id=${details.partnerId}` +
|
||||||
` --user-id=${details.userId}`,
|
` --user-id=${details.userId}`,
|
||||||
this.env()
|
this.env(),
|
||||||
);
|
);
|
||||||
console.log(stdout);
|
console.log(stdout);
|
||||||
}
|
}
|
||||||
@ -480,7 +488,7 @@ export class LibeufinCli {
|
|||||||
` --ebics-host-id=${sd.hostId}` +
|
` --ebics-host-id=${sd.hostId}` +
|
||||||
` --ebics-partner-id=${sd.partnerId}` +
|
` --ebics-partner-id=${sd.partnerId}` +
|
||||||
` --ebics-user-id=${sd.userId}`,
|
` --ebics-user-id=${sd.userId}`,
|
||||||
this.env()
|
this.env(),
|
||||||
);
|
);
|
||||||
console.log(stdout);
|
console.log(stdout);
|
||||||
}
|
}
|
||||||
@ -490,7 +498,7 @@ export class LibeufinCli {
|
|||||||
this.globalTestState,
|
this.globalTestState,
|
||||||
"libeufin-cli-generatetransactions",
|
"libeufin-cli-generatetransactions",
|
||||||
`libeufin-cli sandbox bankaccount generate-transactions ${accountName}`,
|
`libeufin-cli sandbox bankaccount generate-transactions ${accountName}`,
|
||||||
this.env()
|
this.env(),
|
||||||
);
|
);
|
||||||
console.log(stdout);
|
console.log(stdout);
|
||||||
}
|
}
|
||||||
@ -500,7 +508,7 @@ export class LibeufinCli {
|
|||||||
this.globalTestState,
|
this.globalTestState,
|
||||||
"libeufin-cli-showsandboxtransactions",
|
"libeufin-cli-showsandboxtransactions",
|
||||||
`libeufin-cli sandbox bankaccount transactions ${accountName}`,
|
`libeufin-cli sandbox bankaccount transactions ${accountName}`,
|
||||||
this.env()
|
this.env(),
|
||||||
);
|
);
|
||||||
console.log(stdout);
|
console.log(stdout);
|
||||||
}
|
}
|
||||||
@ -834,9 +842,12 @@ export async function launchLibeufinServices(
|
|||||||
libeufinNexus,
|
libeufinNexus,
|
||||||
nb.twgHistoryPermission,
|
nb.twgHistoryPermission,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "anastasis":
|
case "anastasis":
|
||||||
await LibeufinNexusApi.createAnastasisFacade(libeufinNexus, nb.anastasisReq);
|
await LibeufinNexusApi.createAnastasisFacade(
|
||||||
|
libeufinNexus,
|
||||||
|
nb.anastasisReq,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ export async function runLibeufinApiBankaccountTest(t: GlobalTestState) {
|
|||||||
debtorName: "mock2",
|
debtorName: "mock2",
|
||||||
amount: "1",
|
amount: "1",
|
||||||
subject: "mock subject",
|
subject: "mock subject",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
await LibeufinNexusApi.fetchTransactions(nexus, "local-mock");
|
await LibeufinNexusApi.fetchTransactions(nexus, "local-mock");
|
||||||
let transactions = await LibeufinNexusApi.getAccountTransactions(
|
let transactions = await LibeufinNexusApi.getAccountTransactions(
|
||||||
@ -106,4 +106,5 @@ export async function runLibeufinApiBankaccountTest(t: GlobalTestState) {
|
|||||||
let el = findNexusPayment("mock subject", transactions.data);
|
let el = findNexusPayment("mock subject", transactions.data);
|
||||||
t.assertTrue(el instanceof Object);
|
t.assertTrue(el instanceof Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
runLibeufinApiBankaccountTest.suites = ["libeufin"];
|
runLibeufinApiBankaccountTest.suites = ["libeufin"];
|
||||||
|
@ -17,12 +17,7 @@
|
|||||||
/**
|
/**
|
||||||
* Imports.
|
* Imports.
|
||||||
*/
|
*/
|
||||||
import {
|
import { AbsoluteTime, ContractTerms, Duration } from "@gnu-taler/taler-util";
|
||||||
AbsoluteTime,
|
|
||||||
ContractTerms,
|
|
||||||
Duration,
|
|
||||||
durationFromSpec,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import {
|
import {
|
||||||
WalletApiOperation,
|
WalletApiOperation,
|
||||||
HarnessExchangeBankAccount,
|
HarnessExchangeBankAccount,
|
||||||
@ -42,7 +37,7 @@ import {
|
|||||||
LibeufinNexusService,
|
LibeufinNexusService,
|
||||||
LibeufinSandboxApi,
|
LibeufinSandboxApi,
|
||||||
LibeufinSandboxService,
|
LibeufinSandboxService,
|
||||||
} from "../harness/libeufin";
|
} from "../harness/libeufin.js";
|
||||||
|
|
||||||
const exchangeIban = "DE71500105179674997361";
|
const exchangeIban = "DE71500105179674997361";
|
||||||
const customerIban = "DE84500105176881385584";
|
const customerIban = "DE84500105176881385584";
|
||||||
|
@ -22,7 +22,6 @@ import { GlobalTestState } from "../harness/harness.js";
|
|||||||
import {
|
import {
|
||||||
createSimpleTestkudosEnvironment,
|
createSimpleTestkudosEnvironment,
|
||||||
withdrawViaBank,
|
withdrawViaBank,
|
||||||
makeTestPayment,
|
|
||||||
} from "../harness/helpers.js";
|
} from "../harness/helpers.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -55,9 +54,7 @@ export async function runPeerToPeerTest(t: GlobalTestState) {
|
|||||||
const checkResp = await wallet.client.call(
|
const checkResp = await wallet.client.call(
|
||||||
WalletApiOperation.CheckPeerPushPayment,
|
WalletApiOperation.CheckPeerPushPayment,
|
||||||
{
|
{
|
||||||
contractPriv: resp.contractPriv,
|
talerUri: resp.talerUri,
|
||||||
exchangeBaseUrl: resp.exchangeBaseUrl,
|
|
||||||
pursePub: resp.pursePub,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -66,8 +63,7 @@ export async function runPeerToPeerTest(t: GlobalTestState) {
|
|||||||
const acceptResp = await wallet.client.call(
|
const acceptResp = await wallet.client.call(
|
||||||
WalletApiOperation.AcceptPeerPushPayment,
|
WalletApiOperation.AcceptPeerPushPayment,
|
||||||
{
|
{
|
||||||
exchangeBaseUrl: resp.exchangeBaseUrl,
|
peerPushPaymentIncomingId: checkResp.peerPushPaymentIncomingId,
|
||||||
pursePub: resp.pursePub,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -127,36 +127,6 @@ export interface ReserveBankInfo {
|
|||||||
* Exchange payto URI that the bank will use to fund the reserve.
|
* Exchange payto URI that the bank will use to fund the reserve.
|
||||||
*/
|
*/
|
||||||
exchangePaytoUri: string;
|
exchangePaytoUri: string;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A reserve record as stored in the wallet's database.
|
|
||||||
*/
|
|
||||||
export interface ReserveRecord {
|
|
||||||
/**
|
|
||||||
* The reserve public key.
|
|
||||||
*/
|
|
||||||
reservePub: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The reserve private key.
|
|
||||||
*/
|
|
||||||
reservePriv: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The exchange base URL for the reserve.
|
|
||||||
*/
|
|
||||||
exchangeBaseUrl: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Currency of the reserve.
|
|
||||||
*/
|
|
||||||
currency: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Time when the reserve was created.
|
|
||||||
*/
|
|
||||||
timestampCreated: TalerProtocolTimestamp;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the information about this reserve was posted to the bank.
|
* Time when the information about this reserve was posted to the bank.
|
||||||
@ -165,83 +135,14 @@ export interface ReserveRecord {
|
|||||||
*
|
*
|
||||||
* Set to undefined if that hasn't happened yet.
|
* Set to undefined if that hasn't happened yet.
|
||||||
*/
|
*/
|
||||||
timestampReserveInfoPosted: TalerProtocolTimestamp | undefined;
|
timestampReserveInfoPosted?: TalerProtocolTimestamp;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time when the reserve was confirmed by the bank.
|
* Time when the reserve was confirmed by the bank.
|
||||||
*
|
*
|
||||||
* Set to undefined if not confirmed yet.
|
* Set to undefined if not confirmed yet.
|
||||||
*/
|
*/
|
||||||
timestampBankConfirmed: TalerProtocolTimestamp | undefined;
|
timestampBankConfirmed?: TalerProtocolTimestamp;
|
||||||
|
|
||||||
/**
|
|
||||||
* Wire information (as payto URI) for the bank account that
|
|
||||||
* transferred funds for this reserve.
|
|
||||||
*/
|
|
||||||
senderWire?: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Amount that was sent by the user to fund the reserve.
|
|
||||||
*/
|
|
||||||
instructedAmount: AmountJson;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extra state for when this is a withdrawal involving
|
|
||||||
* a Taler-integrated bank.
|
|
||||||
*/
|
|
||||||
bankInfo?: ReserveBankInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restrict withdrawals from this reserve to this age.
|
|
||||||
*/
|
|
||||||
restrictAge?: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pre-allocated ID of the withdrawal group for the first withdrawal
|
|
||||||
* on this reserve.
|
|
||||||
*/
|
|
||||||
initialWithdrawalGroupId: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Did we start the first withdrawal for this reserve?
|
|
||||||
*
|
|
||||||
* We only report a pending withdrawal for the reserve before
|
|
||||||
* the first withdrawal has started.
|
|
||||||
*/
|
|
||||||
initialWithdrawalStarted: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initial denomination selection, stored here so that
|
|
||||||
* we can show this information in the transactions/balances
|
|
||||||
* before we have a withdrawal group.
|
|
||||||
*/
|
|
||||||
initialDenomSel: DenomSelectionState;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Current status of the reserve.
|
|
||||||
*/
|
|
||||||
reserveStatus: ReserveRecordStatus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Is there any work to be done for this reserve?
|
|
||||||
*
|
|
||||||
* Technically redundant, since the reserveStatus would indicate this.
|
|
||||||
* However, we use the operationStatus for DB indexing of pending operations.
|
|
||||||
*/
|
|
||||||
operationStatus: OperationStatus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retry info, in case the reserve needs to be processed again
|
|
||||||
* later, either due to an error or because the wallet needs to
|
|
||||||
* wait for something.
|
|
||||||
*/
|
|
||||||
retryInfo: RetryInfo | undefined;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Last error that happened in a reserve operation
|
|
||||||
* (either talking to the bank or the exchange).
|
|
||||||
*/
|
|
||||||
lastError: TalerErrorDetail | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -514,6 +415,11 @@ export interface ExchangeDetailsPointer {
|
|||||||
updateClock: TalerProtocolTimestamp;
|
updateClock: TalerProtocolTimestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MergeReserveInfo {
|
||||||
|
reservePub: string;
|
||||||
|
reservePriv: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exchange record as stored in the wallet's database.
|
* Exchange record as stored in the wallet's database.
|
||||||
*/
|
*/
|
||||||
@ -568,7 +474,7 @@ export interface ExchangeRecord {
|
|||||||
* Public key of the reserve that we're currently using for
|
* Public key of the reserve that we're currently using for
|
||||||
* receiving P2P payments.
|
* receiving P2P payments.
|
||||||
*/
|
*/
|
||||||
currentMergeReservePub?: string;
|
currentMergeReserveInfo?: MergeReserveInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1373,6 +1279,7 @@ export interface WithdrawalGroupRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Secret seed used to derive planchets.
|
* Secret seed used to derive planchets.
|
||||||
|
* Stored since planchets are created lazily.
|
||||||
*/
|
*/
|
||||||
secretSeed: string;
|
secretSeed: string;
|
||||||
|
|
||||||
@ -1381,6 +1288,11 @@ export interface WithdrawalGroupRecord {
|
|||||||
*/
|
*/
|
||||||
reservePub: string;
|
reservePub: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reserve private key.
|
||||||
|
*/
|
||||||
|
reservePriv: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The exchange base URL that we're withdrawing from.
|
* The exchange base URL that we're withdrawing from.
|
||||||
* (Redundantly stored, as the reserve record also has this info.)
|
* (Redundantly stored, as the reserve record also has this info.)
|
||||||
@ -1395,8 +1307,6 @@ export interface WithdrawalGroupRecord {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* When was the withdrawal operation completed?
|
* When was the withdrawal operation completed?
|
||||||
*
|
|
||||||
* FIXME: We should probably drop this and introduce an OperationStatus field.
|
|
||||||
*/
|
*/
|
||||||
timestampFinish?: TalerProtocolTimestamp;
|
timestampFinish?: TalerProtocolTimestamp;
|
||||||
|
|
||||||
@ -1406,6 +1316,33 @@ export interface WithdrawalGroupRecord {
|
|||||||
*/
|
*/
|
||||||
operationStatus: OperationStatus;
|
operationStatus: OperationStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current status of the reserve.
|
||||||
|
*/
|
||||||
|
reserveStatus: ReserveRecordStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that was sent by the user to fund the reserve.
|
||||||
|
*/
|
||||||
|
instructedAmount: AmountJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire information (as payto URI) for the bank account that
|
||||||
|
* transferred funds for this reserve.
|
||||||
|
*/
|
||||||
|
senderWire?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restrict withdrawals from this reserve to this age.
|
||||||
|
*/
|
||||||
|
restrictAge?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extra state for when this is a withdrawal involving
|
||||||
|
* a Taler-integrated bank.
|
||||||
|
*/
|
||||||
|
bankInfo?: ReserveBankInfo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Amount including fees (i.e. the amount subtracted from the
|
* Amount including fees (i.e. the amount subtracted from the
|
||||||
* reserve to withdraw all coins in this withdrawal session).
|
* reserve to withdraw all coins in this withdrawal session).
|
||||||
@ -1730,9 +1667,11 @@ export interface PeerPushPaymentInitiationRecord {
|
|||||||
/**
|
/**
|
||||||
* Record for a push P2P payment that this wallet was offered.
|
* Record for a push P2P payment that this wallet was offered.
|
||||||
*
|
*
|
||||||
* Primary key: (exchangeBaseUrl, pursePub)
|
* Unique: (exchangeBaseUrl, pursePub)
|
||||||
*/
|
*/
|
||||||
export interface PeerPushPaymentIncomingRecord {
|
export interface PeerPushPaymentIncomingRecord {
|
||||||
|
peerPushPaymentIncomingId: string;
|
||||||
|
|
||||||
exchangeBaseUrl: string;
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
pursePub: string;
|
pursePub: string;
|
||||||
@ -1828,16 +1767,6 @@ export const WalletStoresV1 = {
|
|||||||
}),
|
}),
|
||||||
{},
|
{},
|
||||||
),
|
),
|
||||||
reserves: describeStore(
|
|
||||||
describeContents<ReserveRecord>("reserves", { keyPath: "reservePub" }),
|
|
||||||
{
|
|
||||||
byInitialWithdrawalGroupId: describeIndex(
|
|
||||||
"byInitialWithdrawalGroupId",
|
|
||||||
"initialWithdrawalGroupId",
|
|
||||||
),
|
|
||||||
byStatus: describeIndex("byStatus", "operationStatus"),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
purchases: describeStore(
|
purchases: describeStore(
|
||||||
describeContents<PurchaseRecord>("purchases", { keyPath: "proposalId" }),
|
describeContents<PurchaseRecord>("purchases", { keyPath: "proposalId" }),
|
||||||
{
|
{
|
||||||
@ -1926,9 +1855,14 @@ export const WalletStoresV1 = {
|
|||||||
),
|
),
|
||||||
peerPushPaymentIncoming: describeStore(
|
peerPushPaymentIncoming: describeStore(
|
||||||
describeContents<PeerPushPaymentIncomingRecord>("peerPushPaymentIncoming", {
|
describeContents<PeerPushPaymentIncomingRecord>("peerPushPaymentIncoming", {
|
||||||
keyPath: ["exchangeBaseUrl", "pursePub"],
|
keyPath: "peerPushPaymentIncomingId",
|
||||||
}),
|
}),
|
||||||
{},
|
{
|
||||||
|
byExchangeAndPurse: describeIndex("byExchangeAndPurse", [
|
||||||
|
"exchangeBaseUrl",
|
||||||
|
"pursePub",
|
||||||
|
]),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ export * from "./operations/exchanges.js";
|
|||||||
|
|
||||||
export * from "./bank-api-client.js";
|
export * from "./bank-api-client.js";
|
||||||
|
|
||||||
export * from "./operations/reserves.js";
|
|
||||||
export * from "./operations/withdraw.js";
|
export * from "./operations/withdraw.js";
|
||||||
export * from "./operations/refresh.js";
|
export * from "./operations/refresh.js";
|
||||||
|
|
||||||
|
@ -73,15 +73,6 @@ export interface MerchantOperations {
|
|||||||
): Promise<MerchantInfo>;
|
): Promise<MerchantInfo>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ReserveOperations {
|
|
||||||
processReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
options?: {
|
|
||||||
forceNow?: boolean;
|
|
||||||
},
|
|
||||||
): Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface for exchange-related operations.
|
* Interface for exchange-related operations.
|
||||||
@ -234,7 +225,6 @@ export interface InternalWalletState {
|
|||||||
exchangeOps: ExchangeOperations;
|
exchangeOps: ExchangeOperations;
|
||||||
recoupOps: RecoupOperations;
|
recoupOps: RecoupOperations;
|
||||||
merchantOps: MerchantOperations;
|
merchantOps: MerchantOperations;
|
||||||
reserveOps: ReserveOperations;
|
|
||||||
|
|
||||||
getDenomInfo(
|
getDenomInfo(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
|
@ -88,7 +88,6 @@ export async function exportBackup(
|
|||||||
backupProviders: x.backupProviders,
|
backupProviders: x.backupProviders,
|
||||||
tips: x.tips,
|
tips: x.tips,
|
||||||
recoupGroups: x.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
reserves: x.reserves,
|
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -128,29 +127,6 @@ export async function exportBackup(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
await tx.reserves.iter().forEach((reserve) => {
|
|
||||||
const backupReserve: BackupReserve = {
|
|
||||||
initial_selected_denoms: reserve.initialDenomSel.selectedDenoms.map(
|
|
||||||
(x) => ({
|
|
||||||
count: x.count,
|
|
||||||
denom_pub_hash: x.denomPubHash,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
initial_withdrawal_group_id: reserve.initialWithdrawalGroupId,
|
|
||||||
instructed_amount: Amounts.stringify(reserve.instructedAmount),
|
|
||||||
reserve_priv: reserve.reservePriv,
|
|
||||||
timestamp_created: reserve.timestampCreated,
|
|
||||||
withdrawal_groups:
|
|
||||||
withdrawalGroupsByReserve[reserve.reservePub] ?? [],
|
|
||||||
// FIXME!
|
|
||||||
timestamp_last_activity: reserve.timestampCreated,
|
|
||||||
};
|
|
||||||
const backupReserves = (backupReservesByExchange[
|
|
||||||
reserve.exchangeBaseUrl
|
|
||||||
] ??= []);
|
|
||||||
backupReserves.push(backupReserve);
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.tips.iter().forEach((tip) => {
|
await tx.tips.iter().forEach((tip) => {
|
||||||
backupTips.push({
|
backupTips.push({
|
||||||
exchange_base_url: tip.exchangeBaseUrl,
|
exchange_base_url: tip.exchangeBaseUrl,
|
||||||
|
@ -236,7 +236,6 @@ export async function importBackup(
|
|||||||
backupProviders: x.backupProviders,
|
backupProviders: x.backupProviders,
|
||||||
tips: x.tips,
|
tips: x.tips,
|
||||||
recoupGroups: x.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
reserves: x.reserves,
|
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
tombstones: x.tombstones,
|
tombstones: x.tombstones,
|
||||||
depositGroups: x.depositGroups,
|
depositGroups: x.depositGroups,
|
||||||
@ -427,94 +426,98 @@ export async function importBackup(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const backupReserve of backupExchangeDetails.reserves) {
|
|
||||||
const reservePub =
|
// FIXME: import reserves with new schema
|
||||||
cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
|
|
||||||
const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
|
// for (const backupReserve of backupExchangeDetails.reserves) {
|
||||||
if (tombstoneSet.has(ts)) {
|
// const reservePub =
|
||||||
continue;
|
// cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
|
||||||
}
|
// const ts = makeEventId(TombstoneTag.DeleteReserve, reservePub);
|
||||||
checkLogicInvariant(!!reservePub);
|
// if (tombstoneSet.has(ts)) {
|
||||||
const existingReserve = await tx.reserves.get(reservePub);
|
// continue;
|
||||||
const instructedAmount = Amounts.parseOrThrow(
|
// }
|
||||||
backupReserve.instructed_amount,
|
// checkLogicInvariant(!!reservePub);
|
||||||
);
|
// const existingReserve = await tx.reserves.get(reservePub);
|
||||||
if (!existingReserve) {
|
// const instructedAmount = Amounts.parseOrThrow(
|
||||||
let bankInfo: ReserveBankInfo | undefined;
|
// backupReserve.instructed_amount,
|
||||||
if (backupReserve.bank_info) {
|
// );
|
||||||
bankInfo = {
|
// if (!existingReserve) {
|
||||||
exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
|
// let bankInfo: ReserveBankInfo | undefined;
|
||||||
statusUrl: backupReserve.bank_info.status_url,
|
// if (backupReserve.bank_info) {
|
||||||
confirmUrl: backupReserve.bank_info.confirm_url,
|
// bankInfo = {
|
||||||
};
|
// exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
|
||||||
}
|
// statusUrl: backupReserve.bank_info.status_url,
|
||||||
await tx.reserves.put({
|
// confirmUrl: backupReserve.bank_info.confirm_url,
|
||||||
currency: instructedAmount.currency,
|
// };
|
||||||
instructedAmount,
|
// }
|
||||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
// await tx.reserves.put({
|
||||||
reservePub,
|
// currency: instructedAmount.currency,
|
||||||
reservePriv: backupReserve.reserve_priv,
|
// instructedAmount,
|
||||||
bankInfo,
|
// exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
timestampCreated: backupReserve.timestamp_created,
|
// reservePub,
|
||||||
timestampBankConfirmed:
|
// reservePriv: backupReserve.reserve_priv,
|
||||||
backupReserve.bank_info?.timestamp_bank_confirmed,
|
// bankInfo,
|
||||||
timestampReserveInfoPosted:
|
// timestampCreated: backupReserve.timestamp_created,
|
||||||
backupReserve.bank_info?.timestamp_reserve_info_posted,
|
// timestampBankConfirmed:
|
||||||
senderWire: backupReserve.sender_wire,
|
// backupReserve.bank_info?.timestamp_bank_confirmed,
|
||||||
retryInfo: RetryInfo.reset(),
|
// timestampReserveInfoPosted:
|
||||||
lastError: undefined,
|
// backupReserve.bank_info?.timestamp_reserve_info_posted,
|
||||||
initialWithdrawalGroupId:
|
// senderWire: backupReserve.sender_wire,
|
||||||
backupReserve.initial_withdrawal_group_id,
|
// retryInfo: RetryInfo.reset(),
|
||||||
initialWithdrawalStarted:
|
// lastError: undefined,
|
||||||
backupReserve.withdrawal_groups.length > 0,
|
// initialWithdrawalGroupId:
|
||||||
// FIXME!
|
// backupReserve.initial_withdrawal_group_id,
|
||||||
reserveStatus: ReserveRecordStatus.QueryingStatus,
|
// initialWithdrawalStarted:
|
||||||
initialDenomSel: await getDenomSelStateFromBackup(
|
// backupReserve.withdrawal_groups.length > 0,
|
||||||
tx,
|
// // FIXME!
|
||||||
backupExchangeDetails.base_url,
|
// reserveStatus: ReserveRecordStatus.QueryingStatus,
|
||||||
backupReserve.initial_selected_denoms,
|
// initialDenomSel: await getDenomSelStateFromBackup(
|
||||||
),
|
// tx,
|
||||||
// FIXME!
|
// backupExchangeDetails.base_url,
|
||||||
operationStatus: OperationStatus.Pending,
|
// backupReserve.initial_selected_denoms,
|
||||||
});
|
// ),
|
||||||
}
|
// // FIXME!
|
||||||
for (const backupWg of backupReserve.withdrawal_groups) {
|
// operationStatus: OperationStatus.Pending,
|
||||||
const ts = makeEventId(
|
// });
|
||||||
TombstoneTag.DeleteWithdrawalGroup,
|
// }
|
||||||
backupWg.withdrawal_group_id,
|
// for (const backupWg of backupReserve.withdrawal_groups) {
|
||||||
);
|
// const ts = makeEventId(
|
||||||
if (tombstoneSet.has(ts)) {
|
// TombstoneTag.DeleteWithdrawalGroup,
|
||||||
continue;
|
// backupWg.withdrawal_group_id,
|
||||||
}
|
// );
|
||||||
const existingWg = await tx.withdrawalGroups.get(
|
// if (tombstoneSet.has(ts)) {
|
||||||
backupWg.withdrawal_group_id,
|
// continue;
|
||||||
);
|
// }
|
||||||
if (!existingWg) {
|
// const existingWg = await tx.withdrawalGroups.get(
|
||||||
await tx.withdrawalGroups.put({
|
// backupWg.withdrawal_group_id,
|
||||||
denomsSel: await getDenomSelStateFromBackup(
|
// );
|
||||||
tx,
|
// if (!existingWg) {
|
||||||
backupExchangeDetails.base_url,
|
// await tx.withdrawalGroups.put({
|
||||||
backupWg.selected_denoms,
|
// denomsSel: await getDenomSelStateFromBackup(
|
||||||
),
|
// tx,
|
||||||
exchangeBaseUrl: backupExchangeDetails.base_url,
|
// backupExchangeDetails.base_url,
|
||||||
lastError: undefined,
|
// backupWg.selected_denoms,
|
||||||
rawWithdrawalAmount: Amounts.parseOrThrow(
|
// ),
|
||||||
backupWg.raw_withdrawal_amount,
|
// exchangeBaseUrl: backupExchangeDetails.base_url,
|
||||||
),
|
// lastError: undefined,
|
||||||
reservePub,
|
// rawWithdrawalAmount: Amounts.parseOrThrow(
|
||||||
retryInfo: RetryInfo.reset(),
|
// backupWg.raw_withdrawal_amount,
|
||||||
secretSeed: backupWg.secret_seed,
|
// ),
|
||||||
timestampStart: backupWg.timestamp_created,
|
// reservePub,
|
||||||
timestampFinish: backupWg.timestamp_finish,
|
// retryInfo: RetryInfo.reset(),
|
||||||
withdrawalGroupId: backupWg.withdrawal_group_id,
|
// secretSeed: backupWg.secret_seed,
|
||||||
denomSelUid: backupWg.selected_denoms_id,
|
// timestampStart: backupWg.timestamp_created,
|
||||||
operationStatus: backupWg.timestamp_finish
|
// timestampFinish: backupWg.timestamp_finish,
|
||||||
? OperationStatus.Finished
|
// withdrawalGroupId: backupWg.withdrawal_group_id,
|
||||||
: OperationStatus.Pending,
|
// denomSelUid: backupWg.selected_denoms_id,
|
||||||
});
|
// operationStatus: backupWg.timestamp_finish
|
||||||
}
|
// ? OperationStatus.Finished
|
||||||
}
|
// : OperationStatus.Pending,
|
||||||
}
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const backupProposal of backupBlob.proposals) {
|
for (const backupProposal of backupBlob.proposals) {
|
||||||
@ -920,10 +923,6 @@ export async function importBackup(
|
|||||||
} else if (type === TombstoneTag.DeleteRefund) {
|
} else if (type === TombstoneTag.DeleteRefund) {
|
||||||
// Nothing required, will just prevent display
|
// Nothing required, will just prevent display
|
||||||
// in the transactions list
|
// in the transactions list
|
||||||
} else if (type === TombstoneTag.DeleteReserve) {
|
|
||||||
// FIXME: Once we also have account (=kyc) reserves,
|
|
||||||
// we need to check if the reserve is an account before deleting here
|
|
||||||
await tx.reserves.delete(rest[0]);
|
|
||||||
} else if (type === TombstoneTag.DeleteTip) {
|
} else if (type === TombstoneTag.DeleteTip) {
|
||||||
await tx.tips.delete(rest[0]);
|
await tx.tips.delete(rest[0]);
|
||||||
} else if (type === TombstoneTag.DeleteWithdrawalGroup) {
|
} else if (type === TombstoneTag.DeleteWithdrawalGroup) {
|
||||||
|
@ -41,7 +41,6 @@ interface WalletBalance {
|
|||||||
export async function getBalancesInsideTransaction(
|
export async function getBalancesInsideTransaction(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
tx: GetReadOnlyAccess<{
|
tx: GetReadOnlyAccess<{
|
||||||
reserves: typeof WalletStoresV1.reserves;
|
|
||||||
coins: typeof WalletStoresV1.coins;
|
coins: typeof WalletStoresV1.coins;
|
||||||
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
refreshGroups: typeof WalletStoresV1.refreshGroups;
|
||||||
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
|
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
|
||||||
@ -65,17 +64,6 @@ export async function getBalancesInsideTransaction(
|
|||||||
return balanceStore[currency];
|
return balanceStore[currency];
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize balance to zero, even if we didn't start withdrawing yet.
|
|
||||||
await tx.reserves.iter().forEach((r) => {
|
|
||||||
const b = initBalance(r.currency);
|
|
||||||
if (!r.initialWithdrawalStarted) {
|
|
||||||
b.pendingIncoming = Amounts.add(
|
|
||||||
b.pendingIncoming,
|
|
||||||
r.initialDenomSel.totalCoinValue,
|
|
||||||
).amount;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await tx.coins.iter().forEach((c) => {
|
await tx.coins.iter().forEach((c) => {
|
||||||
// Only count fresh coins, as dormant coins will
|
// Only count fresh coins, as dormant coins will
|
||||||
// already be in a refresh session.
|
// already be in a refresh session.
|
||||||
@ -154,7 +142,6 @@ export async function getBalances(
|
|||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
coins: x.coins,
|
coins: x.coins,
|
||||||
refreshGroups: x.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
reserves: x.reserves,
|
|
||||||
purchases: x.purchases,
|
purchases: x.purchases,
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
}))
|
}))
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
This file is part of GNU Taler
|
This file is part of GNU Taler
|
||||||
(C) 2019 GNUnet e.V.
|
(C) 2022 GNUnet e.V.
|
||||||
|
|
||||||
GNU Taler is free software; you can redistribute it and/or modify it under the
|
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
|
terms of the GNU General Public License as published by the Free Software
|
||||||
@ -30,35 +30,35 @@ import {
|
|||||||
codecForAmountString,
|
codecForAmountString,
|
||||||
codecForAny,
|
codecForAny,
|
||||||
codecForExchangeGetContractResponse,
|
codecForExchangeGetContractResponse,
|
||||||
|
constructPayPushUri,
|
||||||
ContractTermsUtil,
|
ContractTermsUtil,
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
Duration,
|
Duration,
|
||||||
eddsaGetPublic,
|
eddsaGetPublic,
|
||||||
encodeCrock,
|
encodeCrock,
|
||||||
ExchangePurseMergeRequest,
|
ExchangePurseMergeRequest,
|
||||||
|
getRandomBytes,
|
||||||
InitiatePeerPushPaymentRequest,
|
InitiatePeerPushPaymentRequest,
|
||||||
InitiatePeerPushPaymentResponse,
|
InitiatePeerPushPaymentResponse,
|
||||||
j2s,
|
j2s,
|
||||||
Logger,
|
Logger,
|
||||||
|
parsePayPushUri,
|
||||||
strcmp,
|
strcmp,
|
||||||
TalerProtocolTimestamp,
|
TalerProtocolTimestamp,
|
||||||
UnblindedSignature,
|
UnblindedSignature,
|
||||||
WalletAccountMergeFlags,
|
WalletAccountMergeFlags,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { url } from "inspector";
|
|
||||||
import {
|
import {
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
|
MergeReserveInfo,
|
||||||
OperationStatus,
|
OperationStatus,
|
||||||
ReserveRecord,
|
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
|
WithdrawalGroupRecord,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
checkSuccessResponseOrThrow,
|
|
||||||
readSuccessResponseJsonOrThrow,
|
|
||||||
throwUnexpectedRequestError,
|
|
||||||
} from "../util/http.js";
|
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { checkDbInvariant } from "../util/invariants.js";
|
import { checkDbInvariant } from "../util/invariants.js";
|
||||||
|
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
||||||
|
|
||||||
const logger = new Logger("operations/peer-to-peer.ts");
|
const logger = new Logger("operations/peer-to-peer.ts");
|
||||||
|
|
||||||
@ -265,6 +265,10 @@ export async function initiatePeerToPeerPush(
|
|||||||
mergePriv: mergePair.priv,
|
mergePriv: mergePair.priv,
|
||||||
pursePub: pursePair.pub,
|
pursePub: pursePair.pub,
|
||||||
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
||||||
|
talerUri: constructPayPushUri({
|
||||||
|
exchangeBaseUrl: coinSelRes.exchangeBaseUrl,
|
||||||
|
contractPriv: econtractResp.contractPriv,
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,26 +285,19 @@ export async function checkPeerPushPayment(
|
|||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
req: CheckPeerPushPaymentRequest,
|
req: CheckPeerPushPaymentRequest,
|
||||||
): Promise<CheckPeerPushPaymentResponse> {
|
): Promise<CheckPeerPushPaymentResponse> {
|
||||||
const getPurseUrl = new URL(
|
// FIXME: Check if existing record exists!
|
||||||
`purses/${req.pursePub}/deposit`,
|
|
||||||
req.exchangeBaseUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
const contractPub = encodeCrock(
|
const uri = parsePayPushUri(req.talerUri);
|
||||||
eddsaGetPublic(decodeCrock(req.contractPriv)),
|
|
||||||
);
|
|
||||||
|
|
||||||
const purseHttpResp = await ws.http.get(getPurseUrl.href);
|
if (!uri) {
|
||||||
|
throw Error("got invalid taler://pay-push URI");
|
||||||
|
}
|
||||||
|
|
||||||
const purseStatus = await readSuccessResponseJsonOrThrow(
|
const exchangeBaseUrl = uri.exchangeBaseUrl;
|
||||||
purseHttpResp,
|
const contractPriv = uri.contractPriv;
|
||||||
codecForExchangePurseStatus(),
|
const contractPub = encodeCrock(eddsaGetPublic(decodeCrock(contractPriv)));
|
||||||
);
|
|
||||||
|
|
||||||
const getContractUrl = new URL(
|
const getContractUrl = new URL(`contracts/${contractPub}`, exchangeBaseUrl);
|
||||||
`contracts/${contractPub}`,
|
|
||||||
req.exchangeBaseUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
const contractHttpResp = await ws.http.get(getContractUrl.href);
|
const contractHttpResp = await ws.http.get(getContractUrl.href);
|
||||||
|
|
||||||
@ -309,22 +306,36 @@ export async function checkPeerPushPayment(
|
|||||||
codecForExchangeGetContractResponse(),
|
codecForExchangeGetContractResponse(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const pursePub = contractResp.purse_pub;
|
||||||
|
|
||||||
const dec = await ws.cryptoApi.decryptContractForMerge({
|
const dec = await ws.cryptoApi.decryptContractForMerge({
|
||||||
ciphertext: contractResp.econtract,
|
ciphertext: contractResp.econtract,
|
||||||
contractPriv: req.contractPriv,
|
contractPriv: contractPriv,
|
||||||
pursePub: req.pursePub,
|
pursePub: pursePub,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const getPurseUrl = new URL(`purses/${pursePub}/deposit`, exchangeBaseUrl);
|
||||||
|
|
||||||
|
const purseHttpResp = await ws.http.get(getPurseUrl.href);
|
||||||
|
|
||||||
|
const purseStatus = await readSuccessResponseJsonOrThrow(
|
||||||
|
purseHttpResp,
|
||||||
|
codecForExchangePurseStatus(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const peerPushPaymentIncomingId = encodeCrock(getRandomBytes(32));
|
||||||
|
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
peerPushPaymentIncoming: x.peerPushPaymentIncoming,
|
peerPushPaymentIncoming: x.peerPushPaymentIncoming,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
await tx.peerPushPaymentIncoming.add({
|
await tx.peerPushPaymentIncoming.add({
|
||||||
contractPriv: req.contractPriv,
|
peerPushPaymentIncomingId,
|
||||||
exchangeBaseUrl: req.exchangeBaseUrl,
|
contractPriv: contractPriv,
|
||||||
|
exchangeBaseUrl: exchangeBaseUrl,
|
||||||
mergePriv: dec.mergePriv,
|
mergePriv: dec.mergePriv,
|
||||||
pursePub: req.pursePub,
|
pursePub: pursePub,
|
||||||
timestampAccepted: TalerProtocolTimestamp.now(),
|
timestampAccepted: TalerProtocolTimestamp.now(),
|
||||||
contractTerms: dec.contractTerms,
|
contractTerms: dec.contractTerms,
|
||||||
});
|
});
|
||||||
@ -333,6 +344,7 @@ export async function checkPeerPushPayment(
|
|||||||
return {
|
return {
|
||||||
amount: purseStatus.balance,
|
amount: purseStatus.balance,
|
||||||
contractTerms: dec.contractTerms,
|
contractTerms: dec.contractTerms,
|
||||||
|
peerPushPaymentIncomingId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -343,9 +355,9 @@ export function talerPaytoFromExchangeReserve(
|
|||||||
const url = new URL(exchangeBaseUrl);
|
const url = new URL(exchangeBaseUrl);
|
||||||
let proto: string;
|
let proto: string;
|
||||||
if (url.protocol === "http:") {
|
if (url.protocol === "http:") {
|
||||||
proto = "taler+http";
|
proto = "taler-reserve-http";
|
||||||
} else if (url.protocol === "https:") {
|
} else if (url.protocol === "https:") {
|
||||||
proto = "taler";
|
proto = "taler-reserve";
|
||||||
} else {
|
} else {
|
||||||
throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
|
throw Error(`unsupported exchange base URL protocol (${url.protocol})`);
|
||||||
}
|
}
|
||||||
@ -365,69 +377,45 @@ export async function acceptPeerPushPayment(
|
|||||||
const peerInc = await ws.db
|
const peerInc = await ws.db
|
||||||
.mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
|
.mktx((x) => ({ peerPushPaymentIncoming: x.peerPushPaymentIncoming }))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
return tx.peerPushPaymentIncoming.get([
|
return tx.peerPushPaymentIncoming.get(req.peerPushPaymentIncomingId);
|
||||||
req.exchangeBaseUrl,
|
|
||||||
req.pursePub,
|
|
||||||
]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!peerInc) {
|
if (!peerInc) {
|
||||||
throw Error("can't accept unknown incoming p2p push payment");
|
throw Error(
|
||||||
|
`can't accept unknown incoming p2p push payment (${req.peerPushPaymentIncomingId})`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
|
const amount = Amounts.parseOrThrow(peerInc.contractTerms.amount);
|
||||||
|
|
||||||
// We have to create the key pair outside of the transaction,
|
// We have to eagerly create the key pair outside of the transaction,
|
||||||
// due to the async crypto API.
|
// due to the async crypto API.
|
||||||
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
|
const newReservePair = await ws.cryptoApi.createEddsaKeypair({});
|
||||||
|
|
||||||
const reserve: ReserveRecord | undefined = await ws.db
|
const mergeReserveInfo: MergeReserveInfo = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
exchanges: x.exchanges,
|
exchanges: x.exchanges,
|
||||||
reserves: x.reserves,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
const ex = await tx.exchanges.get(req.exchangeBaseUrl);
|
const ex = await tx.exchanges.get(peerInc.exchangeBaseUrl);
|
||||||
checkDbInvariant(!!ex);
|
checkDbInvariant(!!ex);
|
||||||
if (ex.currentMergeReservePub) {
|
if (ex.currentMergeReserveInfo) {
|
||||||
return await tx.reserves.get(ex.currentMergeReservePub);
|
return ex.currentMergeReserveInfo;
|
||||||
}
|
}
|
||||||
const rec: ReserveRecord = {
|
await tx.exchanges.put(ex);
|
||||||
exchangeBaseUrl: req.exchangeBaseUrl,
|
ex.currentMergeReserveInfo = {
|
||||||
// FIXME: field will be removed in the future, folded into withdrawal/p2p record.
|
|
||||||
reserveStatus: ReserveRecordStatus.Dormant,
|
|
||||||
timestampCreated: TalerProtocolTimestamp.now(),
|
|
||||||
instructedAmount: Amounts.getZero(amount.currency),
|
|
||||||
currency: amount.currency,
|
|
||||||
reservePub: newReservePair.pub,
|
|
||||||
reservePriv: newReservePair.priv,
|
reservePriv: newReservePair.priv,
|
||||||
timestampBankConfirmed: undefined,
|
reservePub: newReservePair.pub,
|
||||||
timestampReserveInfoPosted: undefined,
|
|
||||||
// FIXME!
|
|
||||||
initialDenomSel: undefined as any,
|
|
||||||
// FIXME!
|
|
||||||
initialWithdrawalGroupId: "",
|
|
||||||
initialWithdrawalStarted: false,
|
|
||||||
lastError: undefined,
|
|
||||||
operationStatus: OperationStatus.Pending,
|
|
||||||
retryInfo: undefined,
|
|
||||||
bankInfo: undefined,
|
|
||||||
restrictAge: undefined,
|
|
||||||
senderWire: undefined,
|
|
||||||
};
|
};
|
||||||
await tx.reserves.put(rec);
|
return ex.currentMergeReserveInfo;
|
||||||
return rec;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!reserve) {
|
|
||||||
throw Error("can't create reserve");
|
|
||||||
}
|
|
||||||
|
|
||||||
const mergeTimestamp = TalerProtocolTimestamp.now();
|
const mergeTimestamp = TalerProtocolTimestamp.now();
|
||||||
|
|
||||||
const reservePayto = talerPaytoFromExchangeReserve(
|
const reservePayto = talerPaytoFromExchangeReserve(
|
||||||
reserve.exchangeBaseUrl,
|
peerInc.exchangeBaseUrl,
|
||||||
reserve.reservePub,
|
mergeReserveInfo.reservePub,
|
||||||
);
|
);
|
||||||
|
|
||||||
const sigRes = await ws.cryptoApi.signPurseMerge({
|
const sigRes = await ws.cryptoApi.signPurseMerge({
|
||||||
@ -442,12 +430,12 @@ export async function acceptPeerPushPayment(
|
|||||||
purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
|
purseFee: Amounts.stringify(Amounts.getZero(amount.currency)),
|
||||||
pursePub: peerInc.pursePub,
|
pursePub: peerInc.pursePub,
|
||||||
reservePayto,
|
reservePayto,
|
||||||
reservePriv: reserve.reservePriv,
|
reservePriv: mergeReserveInfo.reservePriv,
|
||||||
});
|
});
|
||||||
|
|
||||||
const mergePurseUrl = new URL(
|
const mergePurseUrl = new URL(
|
||||||
`purses/${req.pursePub}/merge`,
|
`purses/${peerInc.pursePub}/merge`,
|
||||||
req.exchangeBaseUrl,
|
peerInc.exchangeBaseUrl,
|
||||||
);
|
);
|
||||||
|
|
||||||
const mergeReq: ExchangePurseMergeRequest = {
|
const mergeReq: ExchangePurseMergeRequest = {
|
||||||
@ -459,6 +447,17 @@ export async function acceptPeerPushPayment(
|
|||||||
|
|
||||||
const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
|
const mergeHttpReq = await ws.http.postJson(mergePurseUrl.href, mergeReq);
|
||||||
|
|
||||||
|
logger.info(`merge request: ${j2s(mergeReq)}`);
|
||||||
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
|
const res = await readSuccessResponseJsonOrThrow(mergeHttpReq, codecForAny());
|
||||||
logger.info(`merge result: ${j2s(res)}`);
|
logger.info(`merge response: ${j2s(res)}`);
|
||||||
|
|
||||||
|
await internalCreateWithdrawalGroup(ws, {
|
||||||
|
amount,
|
||||||
|
exchangeBaseUrl: peerInc.exchangeBaseUrl,
|
||||||
|
reserveStatus: ReserveRecordStatus.QueryingStatus,
|
||||||
|
reserveKeyPair: {
|
||||||
|
priv: mergeReserveInfo.reservePriv,
|
||||||
|
pub: mergeReserveInfo.reservePub,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -70,44 +70,6 @@ async function gatherExchangePending(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function gatherReservePending(
|
|
||||||
tx: GetReadOnlyAccess<{ reserves: typeof WalletStoresV1.reserves }>,
|
|
||||||
now: AbsoluteTime,
|
|
||||||
resp: PendingOperationsResponse,
|
|
||||||
): Promise<void> {
|
|
||||||
const reserves = await tx.reserves.indexes.byStatus.getAll(
|
|
||||||
OperationStatus.Pending,
|
|
||||||
);
|
|
||||||
for (const reserve of reserves) {
|
|
||||||
const reserveType = reserve.bankInfo
|
|
||||||
? ReserveType.TalerBankWithdraw
|
|
||||||
: ReserveType.Manual;
|
|
||||||
switch (reserve.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.Dormant:
|
|
||||||
// nothing to report as pending
|
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
case ReserveRecordStatus.QueryingStatus:
|
|
||||||
case ReserveRecordStatus.RegisteringBank: {
|
|
||||||
resp.pendingOperations.push({
|
|
||||||
type: PendingTaskType.Reserve,
|
|
||||||
givesLifeness: true,
|
|
||||||
timestampDue: reserve.retryInfo?.nextRetry ?? AbsoluteTime.now(),
|
|
||||||
stage: reserve.reserveStatus,
|
|
||||||
timestampCreated: reserve.timestampCreated,
|
|
||||||
reserveType,
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
retryInfo: reserve.retryInfo,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// FIXME: report problem!
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function gatherRefreshPending(
|
async function gatherRefreshPending(
|
||||||
tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
|
tx: GetReadOnlyAccess<{ refreshGroups: typeof WalletStoresV1.refreshGroups }>,
|
||||||
now: AbsoluteTime,
|
now: AbsoluteTime,
|
||||||
@ -336,7 +298,6 @@ export async function getPendingOperations(
|
|||||||
backupProviders: x.backupProviders,
|
backupProviders: x.backupProviders,
|
||||||
exchanges: x.exchanges,
|
exchanges: x.exchanges,
|
||||||
exchangeDetails: x.exchangeDetails,
|
exchangeDetails: x.exchangeDetails,
|
||||||
reserves: x.reserves,
|
|
||||||
refreshGroups: x.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
coins: x.coins,
|
coins: x.coins,
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
@ -352,7 +313,6 @@ export async function getPendingOperations(
|
|||||||
pendingOperations: [],
|
pendingOperations: [],
|
||||||
};
|
};
|
||||||
await gatherExchangePending(tx, now, resp);
|
await gatherExchangePending(tx, now, resp);
|
||||||
await gatherReservePending(tx, now, resp);
|
|
||||||
await gatherRefreshPending(tx, now, resp);
|
await gatherRefreshPending(tx, now, resp);
|
||||||
await gatherWithdrawalPending(tx, now, resp);
|
await gatherWithdrawalPending(tx, now, resp);
|
||||||
await gatherProposalPending(tx, now, resp);
|
await gatherProposalPending(tx, now, resp);
|
||||||
|
@ -26,28 +26,35 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
Amounts,
|
Amounts,
|
||||||
codecForRecoupConfirmation, encodeCrock, getRandomBytes, j2s, Logger, NotificationType,
|
codecForRecoupConfirmation,
|
||||||
|
encodeCrock,
|
||||||
|
getRandomBytes,
|
||||||
|
j2s,
|
||||||
|
Logger,
|
||||||
|
NotificationType,
|
||||||
RefreshReason,
|
RefreshReason,
|
||||||
TalerErrorDetail,
|
TalerErrorDetail,
|
||||||
TalerProtocolTimestamp, URL
|
TalerProtocolTimestamp,
|
||||||
|
URL,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import {
|
import {
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
CoinStatus, OperationStatus, RecoupGroupRecord,
|
CoinStatus,
|
||||||
|
OperationStatus,
|
||||||
|
RecoupGroupRecord,
|
||||||
RefreshCoinSource,
|
RefreshCoinSource,
|
||||||
ReserveRecordStatus, WalletStoresV1, WithdrawCoinSource
|
ReserveRecordStatus,
|
||||||
|
WalletStoresV1,
|
||||||
|
WithdrawCoinSource,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
import { readSuccessResponseJsonOrThrow } from "../util/http.js";
|
||||||
import { GetReadWriteAccess } from "../util/query.js";
|
import { GetReadWriteAccess } from "../util/query.js";
|
||||||
import {
|
import { RetryInfo } from "../util/retries.js";
|
||||||
RetryInfo
|
|
||||||
} from "../util/retries.js";
|
|
||||||
import { guardOperationException } from "./common.js";
|
import { guardOperationException } from "./common.js";
|
||||||
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
|
import { createRefreshGroup, processRefreshGroup } from "./refresh.js";
|
||||||
import { getReserveRequestTimeout, processReserve } from "./reserves.js";
|
import { internalCreateWithdrawalGroup } from "./withdraw.js";
|
||||||
|
|
||||||
|
|
||||||
const logger = new Logger("operations/recoup.ts");
|
const logger = new Logger("operations/recoup.ts");
|
||||||
|
|
||||||
@ -182,34 +189,24 @@ async function recoupWithdrawCoin(
|
|||||||
cs: WithdrawCoinSource,
|
cs: WithdrawCoinSource,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const reservePub = cs.reservePub;
|
const reservePub = cs.reservePub;
|
||||||
const d = await ws.db
|
const denomInfo = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
reserves: x.reserves,
|
|
||||||
denominations: x.denominations,
|
denominations: x.denominations,
|
||||||
}))
|
}))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
const reserve = await tx.reserves.get(reservePub);
|
|
||||||
if (!reserve) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const denomInfo = await ws.getDenomInfo(
|
const denomInfo = await ws.getDenomInfo(
|
||||||
ws,
|
ws,
|
||||||
tx,
|
tx,
|
||||||
reserve.exchangeBaseUrl,
|
coin.exchangeBaseUrl,
|
||||||
coin.denomPubHash,
|
coin.denomPubHash,
|
||||||
);
|
);
|
||||||
if (!denomInfo) {
|
return denomInfo;
|
||||||
return;
|
|
||||||
}
|
|
||||||
return { reserve, denomInfo };
|
|
||||||
});
|
});
|
||||||
if (!d) {
|
if (!denomInfo) {
|
||||||
// FIXME: We should at least emit some pending operation / warning for this?
|
// FIXME: We should at least emit some pending operation / warning for this?
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { reserve, denomInfo } = d;
|
|
||||||
|
|
||||||
ws.notify({
|
ws.notify({
|
||||||
type: NotificationType.RecoupStarted,
|
type: NotificationType.RecoupStarted,
|
||||||
});
|
});
|
||||||
@ -224,9 +221,7 @@ async function recoupWithdrawCoin(
|
|||||||
});
|
});
|
||||||
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
const reqUrl = new URL(`/coins/${coin.coinPub}/recoup`, coin.exchangeBaseUrl);
|
||||||
logger.trace(`requesting recoup via ${reqUrl.href}`);
|
logger.trace(`requesting recoup via ${reqUrl.href}`);
|
||||||
const resp = await ws.http.postJson(reqUrl.href, recoupRequest, {
|
const resp = await ws.http.postJson(reqUrl.href, recoupRequest);
|
||||||
timeout: getReserveRequestTimeout(reserve),
|
|
||||||
});
|
|
||||||
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
const recoupConfirmation = await readSuccessResponseJsonOrThrow(
|
||||||
resp,
|
resp,
|
||||||
codecForRecoupConfirmation(),
|
codecForRecoupConfirmation(),
|
||||||
@ -244,7 +239,6 @@ async function recoupWithdrawCoin(
|
|||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
coins: x.coins,
|
coins: x.coins,
|
||||||
denominations: x.denominations,
|
denominations: x.denominations,
|
||||||
reserves: x.reserves,
|
|
||||||
recoupGroups: x.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
refreshGroups: x.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
}))
|
}))
|
||||||
@ -260,18 +254,12 @@ async function recoupWithdrawCoin(
|
|||||||
if (!updatedCoin) {
|
if (!updatedCoin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const updatedReserve = await tx.reserves.get(reserve.reservePub);
|
|
||||||
if (!updatedReserve) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updatedCoin.status = CoinStatus.Dormant;
|
updatedCoin.status = CoinStatus.Dormant;
|
||||||
const currency = updatedCoin.currentAmount.currency;
|
const currency = updatedCoin.currentAmount.currency;
|
||||||
updatedCoin.currentAmount = Amounts.getZero(currency);
|
updatedCoin.currentAmount = Amounts.getZero(currency);
|
||||||
updatedReserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
|
||||||
updatedReserve.retryInfo = RetryInfo.reset();
|
|
||||||
updatedReserve.operationStatus = OperationStatus.Pending;
|
|
||||||
await tx.coins.put(updatedCoin);
|
await tx.coins.put(updatedCoin);
|
||||||
await tx.reserves.put(updatedReserve);
|
// FIXME: Actually withdraw here!
|
||||||
|
// await internalCreateWithdrawalGroup(ws, {...});
|
||||||
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
await putGroupAsFinished(ws, tx, recoupGroup, coinIdx);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -341,7 +329,6 @@ async function recoupRefreshCoin(
|
|||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
coins: x.coins,
|
coins: x.coins,
|
||||||
denominations: x.denominations,
|
denominations: x.denominations,
|
||||||
reserves: x.reserves,
|
|
||||||
recoupGroups: x.recoupGroups,
|
recoupGroups: x.recoupGroups,
|
||||||
refreshGroups: x.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
}))
|
}))
|
||||||
@ -446,12 +433,6 @@ async function processRecoupGroupImpl(
|
|||||||
reserveSet.add(coin.coinSource.reservePub);
|
reserveSet.add(coin.coinSource.reservePub);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const r of reserveSet.values()) {
|
|
||||||
processReserve(ws, r, { forceNow: true }).catch((e) => {
|
|
||||||
logger.error(`processing reserve ${r} after recoup failed`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createRecoupGroup(
|
export async function createRecoupGroup(
|
||||||
|
@ -1,843 +0,0 @@
|
|||||||
/*
|
|
||||||
This file is part of GNU Taler
|
|
||||||
(C) 2019 GNUnet e.V.
|
|
||||||
|
|
||||||
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 {
|
|
||||||
AbsoluteTime,
|
|
||||||
AcceptWithdrawalResponse,
|
|
||||||
addPaytoQueryParams,
|
|
||||||
Amounts,
|
|
||||||
canonicalizeBaseUrl,
|
|
||||||
codecForBankWithdrawalOperationPostResponse,
|
|
||||||
codecForReserveStatus,
|
|
||||||
codecForWithdrawOperationStatusResponse,
|
|
||||||
CreateReserveRequest,
|
|
||||||
CreateReserveResponse,
|
|
||||||
Duration,
|
|
||||||
durationMax,
|
|
||||||
durationMin,
|
|
||||||
encodeCrock,
|
|
||||||
ForcedDenomSel,
|
|
||||||
getRandomBytes,
|
|
||||||
j2s,
|
|
||||||
Logger,
|
|
||||||
NotificationType,
|
|
||||||
randomBytes,
|
|
||||||
TalerErrorCode,
|
|
||||||
TalerErrorDetail,
|
|
||||||
URL,
|
|
||||||
} from "@gnu-taler/taler-util";
|
|
||||||
import {
|
|
||||||
DenomSelectionState,
|
|
||||||
OperationStatus,
|
|
||||||
ReserveBankInfo,
|
|
||||||
ReserveRecord,
|
|
||||||
ReserveRecordStatus,
|
|
||||||
WalletStoresV1,
|
|
||||||
WithdrawalGroupRecord,
|
|
||||||
} from "../db.js";
|
|
||||||
import { TalerError } from "../errors.js";
|
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
|
||||||
import { assertUnreachable } from "../util/assertUnreachable.js";
|
|
||||||
import {
|
|
||||||
readSuccessResponseJsonOrErrorCode,
|
|
||||||
readSuccessResponseJsonOrThrow,
|
|
||||||
throwUnexpectedRequestError,
|
|
||||||
} from "../util/http.js";
|
|
||||||
import { GetReadOnlyAccess } from "../util/query.js";
|
|
||||||
import { RetryInfo } from "../util/retries.js";
|
|
||||||
import { guardOperationException } from "./common.js";
|
|
||||||
import {
|
|
||||||
getExchangeDetails,
|
|
||||||
getExchangePaytoUri,
|
|
||||||
getExchangeTrust,
|
|
||||||
updateExchangeFromUrl,
|
|
||||||
} from "./exchanges.js";
|
|
||||||
import {
|
|
||||||
getBankWithdrawalInfo,
|
|
||||||
getCandidateWithdrawalDenoms,
|
|
||||||
processWithdrawGroup,
|
|
||||||
selectForcedWithdrawalDenominations,
|
|
||||||
selectWithdrawalDenominations,
|
|
||||||
updateWithdrawalDenoms,
|
|
||||||
} from "./withdraw.js";
|
|
||||||
|
|
||||||
const logger = new Logger("taler-wallet-core:reserves.ts");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up the reserve's retry timeout in preparation for
|
|
||||||
* processing the reserve.
|
|
||||||
*/
|
|
||||||
async function setupReserveRetry(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
options: {
|
|
||||||
reset: boolean;
|
|
||||||
},
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const r = await tx.reserves.get(reservePub);
|
|
||||||
if (!r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (options.reset) {
|
|
||||||
r.retryInfo = RetryInfo.reset();
|
|
||||||
} else {
|
|
||||||
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
|
||||||
}
|
|
||||||
delete r.lastError;
|
|
||||||
await tx.reserves.put(r);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Report an error that happened while processing the reserve.
|
|
||||||
*
|
|
||||||
* Logs the error via a notification and by storing it in the database.
|
|
||||||
*/
|
|
||||||
async function reportReserveError(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
err: TalerErrorDetail,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const r = await tx.reserves.get(reservePub);
|
|
||||||
if (!r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!r.retryInfo) {
|
|
||||||
logger.error(`got reserve error for inactive reserve (no retryInfo)`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
r.lastError = err;
|
|
||||||
await tx.reserves.put(r);
|
|
||||||
});
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.ReserveOperationError,
|
|
||||||
error: err,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a reserve, but do not flag it as confirmed yet.
|
|
||||||
*
|
|
||||||
* Adds the corresponding exchange as a trusted exchange if it is neither
|
|
||||||
* audited nor trusted already.
|
|
||||||
*/
|
|
||||||
export async function createReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
req: CreateReserveRequest,
|
|
||||||
): Promise<CreateReserveResponse> {
|
|
||||||
const keypair = await ws.cryptoApi.createEddsaKeypair({});
|
|
||||||
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
|
||||||
const canonExchange = canonicalizeBaseUrl(req.exchange);
|
|
||||||
|
|
||||||
let reserveStatus;
|
|
||||||
if (req.bankWithdrawStatusUrl) {
|
|
||||||
reserveStatus = ReserveRecordStatus.RegisteringBank;
|
|
||||||
} else {
|
|
||||||
reserveStatus = ReserveRecordStatus.QueryingStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bankInfo: ReserveBankInfo | undefined;
|
|
||||||
|
|
||||||
if (req.bankWithdrawStatusUrl) {
|
|
||||||
if (!req.exchangePaytoUri) {
|
|
||||||
throw Error(
|
|
||||||
"Exchange payto URI must be specified for a bank-integrated withdrawal",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
bankInfo = {
|
|
||||||
statusUrl: req.bankWithdrawStatusUrl,
|
|
||||||
exchangePaytoUri: req.exchangePaytoUri,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialWithdrawalGroupId = encodeCrock(getRandomBytes(32));
|
|
||||||
|
|
||||||
await updateWithdrawalDenoms(ws, canonExchange);
|
|
||||||
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
|
|
||||||
|
|
||||||
let initialDenomSel: DenomSelectionState;
|
|
||||||
if (req.forcedDenomSel) {
|
|
||||||
logger.warn("using forced denom selection");
|
|
||||||
initialDenomSel = selectForcedWithdrawalDenominations(
|
|
||||||
req.amount,
|
|
||||||
denoms,
|
|
||||||
req.forcedDenomSel,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
initialDenomSel = selectWithdrawalDenominations(req.amount, denoms);
|
|
||||||
}
|
|
||||||
|
|
||||||
const reserveRecord: ReserveRecord = {
|
|
||||||
instructedAmount: req.amount,
|
|
||||||
initialWithdrawalGroupId,
|
|
||||||
initialDenomSel,
|
|
||||||
initialWithdrawalStarted: false,
|
|
||||||
timestampCreated: now,
|
|
||||||
exchangeBaseUrl: canonExchange,
|
|
||||||
reservePriv: keypair.priv,
|
|
||||||
reservePub: keypair.pub,
|
|
||||||
senderWire: req.senderWire,
|
|
||||||
timestampBankConfirmed: undefined,
|
|
||||||
timestampReserveInfoPosted: undefined,
|
|
||||||
bankInfo,
|
|
||||||
reserveStatus,
|
|
||||||
retryInfo: RetryInfo.reset(),
|
|
||||||
lastError: undefined,
|
|
||||||
currency: req.amount.currency,
|
|
||||||
operationStatus: OperationStatus.Pending,
|
|
||||||
restrictAge: req.restrictAge,
|
|
||||||
};
|
|
||||||
|
|
||||||
const exchangeInfo = await updateExchangeFromUrl(ws, req.exchange);
|
|
||||||
const exchangeDetails = exchangeInfo.exchangeDetails;
|
|
||||||
if (!exchangeDetails) {
|
|
||||||
logger.trace(exchangeDetails);
|
|
||||||
throw Error("exchange not updated");
|
|
||||||
}
|
|
||||||
const { isAudited, isTrusted } = await getExchangeTrust(
|
|
||||||
ws,
|
|
||||||
exchangeInfo.exchange,
|
|
||||||
);
|
|
||||||
|
|
||||||
const resp = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
exchangeTrust: x.exchangeTrust,
|
|
||||||
reserves: x.reserves,
|
|
||||||
bankWithdrawUris: x.bankWithdrawUris,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
// Check if we have already created a reserve for that bankWithdrawStatusUrl
|
|
||||||
if (reserveRecord.bankInfo?.statusUrl) {
|
|
||||||
const bwi = await tx.bankWithdrawUris.get(
|
|
||||||
reserveRecord.bankInfo.statusUrl,
|
|
||||||
);
|
|
||||||
if (bwi) {
|
|
||||||
const otherReserve = await tx.reserves.get(bwi.reservePub);
|
|
||||||
if (otherReserve) {
|
|
||||||
logger.trace(
|
|
||||||
"returning existing reserve for bankWithdrawStatusUri",
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
exchange: otherReserve.exchangeBaseUrl,
|
|
||||||
reservePub: otherReserve.reservePub,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await tx.bankWithdrawUris.put({
|
|
||||||
reservePub: reserveRecord.reservePub,
|
|
||||||
talerWithdrawUri: reserveRecord.bankInfo.statusUrl,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!isAudited && !isTrusted) {
|
|
||||||
await tx.exchangeTrust.put({
|
|
||||||
currency: reserveRecord.currency,
|
|
||||||
exchangeBaseUrl: reserveRecord.exchangeBaseUrl,
|
|
||||||
exchangeMasterPub: exchangeDetails.masterPublicKey,
|
|
||||||
uids: [encodeCrock(getRandomBytes(32))],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await tx.reserves.put(reserveRecord);
|
|
||||||
const r: CreateReserveResponse = {
|
|
||||||
exchange: canonExchange,
|
|
||||||
reservePub: keypair.pub,
|
|
||||||
};
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (reserveRecord.reservePub === resp.reservePub) {
|
|
||||||
// Only emit notification when a new reserve was created.
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.ReserveCreated,
|
|
||||||
reservePub: reserveRecord.reservePub,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asynchronously process the reserve, but return
|
|
||||||
// to the caller already.
|
|
||||||
processReserve(ws, resp.reservePub, { forceNow: true }).catch((e) => {
|
|
||||||
logger.error("Processing reserve (after createReserve) failed:", e);
|
|
||||||
});
|
|
||||||
|
|
||||||
return resp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-query the status of a reserve.
|
|
||||||
*/
|
|
||||||
export async function forceQueryReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
): Promise<void> {
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const reserve = await tx.reserves.get(reservePub);
|
|
||||||
if (!reserve) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Only force status query where it makes sense
|
|
||||||
switch (reserve.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.Dormant:
|
|
||||||
reserve.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
|
||||||
reserve.operationStatus = OperationStatus.Pending;
|
|
||||||
reserve.retryInfo = RetryInfo.reset();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await tx.reserves.put(reserve);
|
|
||||||
});
|
|
||||||
await processReserve(ws, reservePub, { forceNow: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* First fetch information required to withdraw from the reserve,
|
|
||||||
* then deplete the reserve, withdrawing coins until it is empty.
|
|
||||||
*
|
|
||||||
* The returned promise resolves once the reserve is set to the
|
|
||||||
* state "Dormant".
|
|
||||||
*/
|
|
||||||
export async function processReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
options: {
|
|
||||||
forceNow?: boolean;
|
|
||||||
} = {},
|
|
||||||
): Promise<void> {
|
|
||||||
return ws.memoProcessReserve.memo(reservePub, async () => {
|
|
||||||
const onOpError = (err: TalerErrorDetail): Promise<void> =>
|
|
||||||
reportReserveError(ws, reservePub, err);
|
|
||||||
await guardOperationException(
|
|
||||||
() => processReserveImpl(ws, reservePub, options),
|
|
||||||
onOpError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function registerReserveWithBank(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const reserve = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
return await tx.reserves.get(reservePub);
|
|
||||||
});
|
|
||||||
switch (reserve?.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bankInfo = reserve.bankInfo;
|
|
||||||
if (!bankInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bankStatusUrl = bankInfo.statusUrl;
|
|
||||||
const httpResp = await ws.http.postJson(
|
|
||||||
bankStatusUrl,
|
|
||||||
{
|
|
||||||
reserve_pub: reservePub,
|
|
||||||
selected_exchange: bankInfo.exchangePaytoUri,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timeout: getReserveRequestTimeout(reserve),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
await readSuccessResponseJsonOrThrow(
|
|
||||||
httpResp,
|
|
||||||
codecForBankWithdrawalOperationPostResponse(),
|
|
||||||
);
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const r = await tx.reserves.get(reservePub);
|
|
||||||
if (!r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (r.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
r.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
|
|
||||||
AbsoluteTime.now(),
|
|
||||||
);
|
|
||||||
r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
|
|
||||||
r.operationStatus = OperationStatus.Pending;
|
|
||||||
if (!r.bankInfo) {
|
|
||||||
throw Error("invariant failed");
|
|
||||||
}
|
|
||||||
r.retryInfo = RetryInfo.reset();
|
|
||||||
await tx.reserves.put(r);
|
|
||||||
});
|
|
||||||
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
|
||||||
return processReserveBankStatus(ws, reservePub);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getReserveRequestTimeout(r: ReserveRecord): Duration {
|
|
||||||
return durationMax(
|
|
||||||
{ d_ms: 60000 },
|
|
||||||
durationMin({ d_ms: 5000 }, RetryInfo.getDuration(r.retryInfo)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processReserveBankStatus(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
): Promise<void> {
|
|
||||||
const reserve = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
return tx.reserves.get(reservePub);
|
|
||||||
});
|
|
||||||
switch (reserve?.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const bankStatusUrl = reserve.bankInfo?.statusUrl;
|
|
||||||
if (!bankStatusUrl) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const statusResp = await ws.http.get(bankStatusUrl, {
|
|
||||||
timeout: getReserveRequestTimeout(reserve),
|
|
||||||
});
|
|
||||||
const status = await readSuccessResponseJsonOrThrow(
|
|
||||||
statusResp,
|
|
||||||
codecForWithdrawOperationStatusResponse(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status.aborted) {
|
|
||||||
logger.info("bank aborted the withdrawal");
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const r = await tx.reserves.get(reservePub);
|
|
||||||
if (!r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (r.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
|
||||||
r.timestampBankConfirmed = now;
|
|
||||||
r.reserveStatus = ReserveRecordStatus.BankAborted;
|
|
||||||
r.operationStatus = OperationStatus.Finished;
|
|
||||||
r.retryInfo = RetryInfo.reset();
|
|
||||||
await tx.reserves.put(r);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bank still needs to know our reserve info
|
|
||||||
if (!status.selection_done) {
|
|
||||||
await registerReserveWithBank(ws, reservePub);
|
|
||||||
return await processReserveBankStatus(ws, reservePub);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Why do we do this?!
|
|
||||||
if (reserve.reserveStatus === ReserveRecordStatus.RegisteringBank) {
|
|
||||||
await registerReserveWithBank(ws, reservePub);
|
|
||||||
return await processReserveBankStatus(ws, reservePub);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const r = await tx.reserves.get(reservePub);
|
|
||||||
if (!r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Re-check reserve status within transaction
|
|
||||||
switch (r.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (status.transfer_done) {
|
|
||||||
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
|
||||||
r.timestampBankConfirmed = now;
|
|
||||||
r.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
|
||||||
r.operationStatus = OperationStatus.Pending;
|
|
||||||
r.retryInfo = RetryInfo.reset();
|
|
||||||
} else {
|
|
||||||
logger.info("Withdrawal operation not yet confirmed by bank");
|
|
||||||
if (r.bankInfo) {
|
|
||||||
r.bankInfo.confirmUrl = status.confirm_transfer_url;
|
|
||||||
}
|
|
||||||
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
|
||||||
}
|
|
||||||
await tx.reserves.put(r);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the information about a reserve that is stored in the wallet
|
|
||||||
* by querying the reserve's exchange.
|
|
||||||
*
|
|
||||||
* If the reserve have funds that are not allocated in a withdrawal group yet
|
|
||||||
* and are big enough to withdraw with available denominations,
|
|
||||||
* create a new withdrawal group for the remaining amount.
|
|
||||||
*/
|
|
||||||
async function updateReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
): Promise<{ ready: boolean }> {
|
|
||||||
const reserve = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
return tx.reserves.get(reservePub);
|
|
||||||
});
|
|
||||||
if (!reserve) {
|
|
||||||
throw Error("reserve not in db");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reserve.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
|
|
||||||
return { ready: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const reserveUrl = new URL(`reserves/${reservePub}`, reserve.exchangeBaseUrl);
|
|
||||||
reserveUrl.searchParams.set("timeout_ms", "30000");
|
|
||||||
|
|
||||||
logger.info(`querying reserve status via ${reserveUrl}`);
|
|
||||||
|
|
||||||
const resp = await ws.http.get(reserveUrl.href, {
|
|
||||||
timeout: getReserveRequestTimeout(reserve),
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await readSuccessResponseJsonOrErrorCode(
|
|
||||||
resp,
|
|
||||||
codecForReserveStatus(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.isError) {
|
|
||||||
if (
|
|
||||||
resp.status === 404 &&
|
|
||||||
result.talerErrorResponse.code ===
|
|
||||||
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
|
||||||
) {
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.ReserveNotYetFound,
|
|
||||||
reservePub,
|
|
||||||
});
|
|
||||||
return { ready: false };
|
|
||||||
} else {
|
|
||||||
throwUnexpectedRequestError(resp, result.talerErrorResponse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.trace(`got reserve status ${j2s(result.response)}`);
|
|
||||||
|
|
||||||
const reserveInfo = result.response;
|
|
||||||
const reserveBalance = Amounts.parseOrThrow(reserveInfo.balance);
|
|
||||||
const currency = reserveBalance.currency;
|
|
||||||
|
|
||||||
await updateWithdrawalDenoms(ws, reserve.exchangeBaseUrl);
|
|
||||||
const denoms = await getCandidateWithdrawalDenoms(
|
|
||||||
ws,
|
|
||||||
reserve.exchangeBaseUrl,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newWithdrawalGroup = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
planchets: x.planchets,
|
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
|
||||||
reserves: x.reserves,
|
|
||||||
denominations: x.denominations,
|
|
||||||
}))
|
|
||||||
.runReadWrite(async (tx) => {
|
|
||||||
const newReserve = await tx.reserves.get(reserve.reservePub);
|
|
||||||
if (!newReserve) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let amountReservePlus = reserveBalance;
|
|
||||||
let amountReserveMinus = Amounts.getZero(currency);
|
|
||||||
|
|
||||||
// Subtract amount allocated in unfinished withdrawal groups
|
|
||||||
// for this reserve from the available amount.
|
|
||||||
await tx.withdrawalGroups.indexes.byReservePub
|
|
||||||
.iter(reservePub)
|
|
||||||
.forEachAsync(async (wg) => {
|
|
||||||
if (wg.timestampFinish) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await tx.planchets.indexes.byGroup
|
|
||||||
.iter(wg.withdrawalGroupId)
|
|
||||||
.forEachAsync(async (pr) => {
|
|
||||||
if (pr.withdrawalDone) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const denomInfo = await ws.getDenomInfo(
|
|
||||||
ws,
|
|
||||||
tx,
|
|
||||||
wg.exchangeBaseUrl,
|
|
||||||
pr.denomPubHash,
|
|
||||||
);
|
|
||||||
if (!denomInfo) {
|
|
||||||
logger.error(`no denom info found for ${pr.denomPubHash}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
amountReserveMinus = Amounts.add(
|
|
||||||
amountReserveMinus,
|
|
||||||
denomInfo.value,
|
|
||||||
denomInfo.feeWithdraw,
|
|
||||||
).amount;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const remainingAmount = Amounts.sub(
|
|
||||||
amountReservePlus,
|
|
||||||
amountReserveMinus,
|
|
||||||
).amount;
|
|
||||||
|
|
||||||
let withdrawalGroupId: string;
|
|
||||||
let denomSel: DenomSelectionState;
|
|
||||||
|
|
||||||
if (!newReserve.initialWithdrawalStarted) {
|
|
||||||
withdrawalGroupId = newReserve.initialWithdrawalGroupId;
|
|
||||||
newReserve.initialWithdrawalStarted = true;
|
|
||||||
denomSel = newReserve.initialDenomSel;
|
|
||||||
} else {
|
|
||||||
withdrawalGroupId = encodeCrock(randomBytes(32));
|
|
||||||
|
|
||||||
denomSel = selectWithdrawalDenominations(remainingAmount, denoms);
|
|
||||||
|
|
||||||
logger.trace(
|
|
||||||
`Remaining unclaimed amount in reseve is ${Amounts.stringify(
|
|
||||||
remainingAmount,
|
|
||||||
)} and can be withdrawn with ${denomSel.selectedDenoms.length} coins`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (denomSel.selectedDenoms.length === 0) {
|
|
||||||
newReserve.reserveStatus = ReserveRecordStatus.Dormant;
|
|
||||||
newReserve.operationStatus = OperationStatus.Finished;
|
|
||||||
delete newReserve.lastError;
|
|
||||||
delete newReserve.retryInfo;
|
|
||||||
await tx.reserves.put(newReserve);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const withdrawalRecord: WithdrawalGroupRecord = {
|
|
||||||
withdrawalGroupId: withdrawalGroupId,
|
|
||||||
exchangeBaseUrl: reserve.exchangeBaseUrl,
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
rawWithdrawalAmount: remainingAmount,
|
|
||||||
timestampStart: AbsoluteTime.toTimestamp(AbsoluteTime.now()),
|
|
||||||
retryInfo: RetryInfo.reset(),
|
|
||||||
lastError: undefined,
|
|
||||||
denomsSel: denomSel,
|
|
||||||
secretSeed: encodeCrock(getRandomBytes(64)),
|
|
||||||
denomSelUid: encodeCrock(getRandomBytes(32)),
|
|
||||||
operationStatus: OperationStatus.Pending,
|
|
||||||
};
|
|
||||||
|
|
||||||
delete newReserve.lastError;
|
|
||||||
delete newReserve.retryInfo;
|
|
||||||
newReserve.reserveStatus = ReserveRecordStatus.Dormant;
|
|
||||||
newReserve.operationStatus = OperationStatus.Finished;
|
|
||||||
|
|
||||||
await tx.reserves.put(newReserve);
|
|
||||||
await tx.withdrawalGroups.put(withdrawalRecord);
|
|
||||||
return withdrawalRecord;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (newWithdrawalGroup) {
|
|
||||||
logger.trace("processing new withdraw group");
|
|
||||||
ws.notify({
|
|
||||||
type: NotificationType.WithdrawGroupCreated,
|
|
||||||
withdrawalGroupId: newWithdrawalGroup.withdrawalGroupId,
|
|
||||||
});
|
|
||||||
await processWithdrawGroup(ws, newWithdrawalGroup.withdrawalGroupId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { ready: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processReserveImpl(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
reservePub: string,
|
|
||||||
options: {
|
|
||||||
forceNow?: boolean;
|
|
||||||
} = {},
|
|
||||||
): Promise<void> {
|
|
||||||
const forceNow = options.forceNow ?? false;
|
|
||||||
await setupReserveRetry(ws, reservePub, { reset: forceNow });
|
|
||||||
const reserve = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
return tx.reserves.get(reservePub);
|
|
||||||
});
|
|
||||||
if (!reserve) {
|
|
||||||
logger.error(
|
|
||||||
`not processing reserve: reserve ${reservePub} does not exist`,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.trace(
|
|
||||||
`Processing reserve ${reservePub} with status ${reserve.reserveStatus}`,
|
|
||||||
);
|
|
||||||
switch (reserve.reserveStatus) {
|
|
||||||
case ReserveRecordStatus.RegisteringBank:
|
|
||||||
await processReserveBankStatus(ws, reservePub);
|
|
||||||
return await processReserveImpl(ws, reservePub, { forceNow: true });
|
|
||||||
case ReserveRecordStatus.QueryingStatus: {
|
|
||||||
const res = await updateReserve(ws, reservePub);
|
|
||||||
if (res.ready) {
|
|
||||||
return await processReserveImpl(ws, reservePub, { forceNow: true });
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ReserveRecordStatus.Dormant:
|
|
||||||
// nothing to do
|
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.WaitConfirmBank:
|
|
||||||
await processReserveBankStatus(ws, reservePub);
|
|
||||||
break;
|
|
||||||
case ReserveRecordStatus.BankAborted:
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn("unknown reserve record status:", reserve.reserveStatus);
|
|
||||||
assertUnreachable(reserve.reserveStatus);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a reserve for a bank-integrated withdrawal from
|
|
||||||
* a taler://withdraw URI.
|
|
||||||
*/
|
|
||||||
export async function createTalerWithdrawReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
talerWithdrawUri: string,
|
|
||||||
selectedExchange: string,
|
|
||||||
options: {
|
|
||||||
forcedDenomSel?: ForcedDenomSel;
|
|
||||||
restrictAge?: number;
|
|
||||||
} = {},
|
|
||||||
): Promise<AcceptWithdrawalResponse> {
|
|
||||||
await updateExchangeFromUrl(ws, selectedExchange);
|
|
||||||
const withdrawInfo = await getBankWithdrawalInfo(ws.http, talerWithdrawUri);
|
|
||||||
const exchangePaytoUri = await getExchangePaytoUri(
|
|
||||||
ws,
|
|
||||||
selectedExchange,
|
|
||||||
withdrawInfo.wireTypes,
|
|
||||||
);
|
|
||||||
const reserve = await createReserve(ws, {
|
|
||||||
amount: withdrawInfo.amount,
|
|
||||||
bankWithdrawStatusUrl: withdrawInfo.extractedStatusUrl,
|
|
||||||
exchange: selectedExchange,
|
|
||||||
senderWire: withdrawInfo.senderWire,
|
|
||||||
exchangePaytoUri: exchangePaytoUri,
|
|
||||||
restrictAge: options.restrictAge,
|
|
||||||
forcedDenomSel: options.forcedDenomSel,
|
|
||||||
});
|
|
||||||
// We do this here, as the reserve should be registered before we return,
|
|
||||||
// so that we can redirect the user to the bank's status page.
|
|
||||||
await processReserveBankStatus(ws, reserve.reservePub);
|
|
||||||
const processedReserve = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
return tx.reserves.get(reserve.reservePub);
|
|
||||||
});
|
|
||||||
if (processedReserve?.reserveStatus === ReserveRecordStatus.BankAborted) {
|
|
||||||
throw TalerError.fromDetail(
|
|
||||||
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
reservePub: reserve.reservePub,
|
|
||||||
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get payto URIs that can be used to fund a reserve.
|
|
||||||
*/
|
|
||||||
export async function getFundingPaytoUris(
|
|
||||||
tx: GetReadOnlyAccess<{
|
|
||||||
reserves: typeof WalletStoresV1.reserves;
|
|
||||||
exchanges: typeof WalletStoresV1.exchanges;
|
|
||||||
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
|
||||||
}>,
|
|
||||||
reservePub: string,
|
|
||||||
): Promise<string[]> {
|
|
||||||
const r = await tx.reserves.get(reservePub);
|
|
||||||
if (!r) {
|
|
||||||
logger.error(`reserve ${reservePub} not found (DB corrupted?)`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const exchangeDetails = await getExchangeDetails(tx, r.exchangeBaseUrl);
|
|
||||||
if (!exchangeDetails) {
|
|
||||||
logger.error(`exchange ${r.exchangeBaseUrl} not found (DB corrupted?)`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const plainPaytoUris =
|
|
||||||
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
|
|
||||||
if (!plainPaytoUris) {
|
|
||||||
logger.error(`exchange ${r.exchangeBaseUrl} has no wire info`);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
return plainPaytoUris.map((x) =>
|
|
||||||
addPaytoQueryParams(x, {
|
|
||||||
amount: Amounts.stringify(r.instructedAmount),
|
|
||||||
message: `Taler Withdrawal ${r.reservePub}`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
@ -39,12 +39,12 @@ import {
|
|||||||
URL,
|
URL,
|
||||||
PreparePayResultType,
|
PreparePayResultType,
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
import { createTalerWithdrawReserve } from "./reserves.js";
|
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
import { confirmPay, preparePayForUri } from "./pay.js";
|
import { confirmPay, preparePayForUri } from "./pay.js";
|
||||||
import { getBalances } from "./balance.js";
|
import { getBalances } from "./balance.js";
|
||||||
import { applyRefund } from "./refund.js";
|
import { applyRefund } from "./refund.js";
|
||||||
import { checkLogicInvariant } from "../util/invariants.js";
|
import { checkLogicInvariant } from "../util/invariants.js";
|
||||||
|
import { acceptWithdrawalFromUri } from "./withdraw.js";
|
||||||
|
|
||||||
const logger = new Logger("operations/testing.ts");
|
const logger = new Logger("operations/testing.ts");
|
||||||
|
|
||||||
@ -104,14 +104,11 @@ export async function withdrawTestBalance(
|
|||||||
amount,
|
amount,
|
||||||
);
|
);
|
||||||
|
|
||||||
await createTalerWithdrawReserve(
|
await acceptWithdrawalFromUri(ws, {
|
||||||
ws,
|
talerWithdrawUri: wresp.taler_withdraw_uri,
|
||||||
wresp.taler_withdraw_uri,
|
selectedExchange: exchangeBaseUrl,
|
||||||
exchangeBaseUrl,
|
forcedDenomSel: req.forcedDenomSel,
|
||||||
{
|
});
|
||||||
forcedDenomSel: req.forcedDenomSel,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await confirmBankWithdrawalUri(
|
await confirmBankWithdrawalUri(
|
||||||
ws.http,
|
ws.http,
|
||||||
|
@ -36,7 +36,6 @@ import { InternalWalletState } from "../internal-wallet-state.js";
|
|||||||
import {
|
import {
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
RefundState,
|
RefundState,
|
||||||
ReserveRecord,
|
|
||||||
ReserveRecordStatus,
|
ReserveRecordStatus,
|
||||||
WalletRefundItem,
|
WalletRefundItem,
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
@ -44,9 +43,8 @@ import { processDepositGroup } from "./deposits.js";
|
|||||||
import { getExchangeDetails } from "./exchanges.js";
|
import { getExchangeDetails } from "./exchanges.js";
|
||||||
import { processPurchasePay } from "./pay.js";
|
import { processPurchasePay } from "./pay.js";
|
||||||
import { processRefreshGroup } from "./refresh.js";
|
import { processRefreshGroup } from "./refresh.js";
|
||||||
import { getFundingPaytoUris } from "./reserves.js";
|
|
||||||
import { processTip } from "./tip.js";
|
import { processTip } from "./tip.js";
|
||||||
import { processWithdrawGroup } from "./withdraw.js";
|
import { processWithdrawalGroup } from "./withdraw.js";
|
||||||
|
|
||||||
const logger = new Logger("taler-wallet-core:transactions.ts");
|
const logger = new Logger("taler-wallet-core:transactions.ts");
|
||||||
|
|
||||||
@ -127,7 +125,6 @@ export async function getTransactions(
|
|||||||
proposals: x.proposals,
|
proposals: x.proposals,
|
||||||
purchases: x.purchases,
|
purchases: x.purchases,
|
||||||
refreshGroups: x.refreshGroups,
|
refreshGroups: x.refreshGroups,
|
||||||
reserves: x.reserves,
|
|
||||||
tips: x.tips,
|
tips: x.tips,
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
planchets: x.planchets,
|
planchets: x.planchets,
|
||||||
@ -151,24 +148,13 @@ export async function getTransactions(
|
|||||||
if (shouldSkipSearch(transactionsRequest, [])) {
|
if (shouldSkipSearch(transactionsRequest, [])) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = await tx.reserves.get(wsr.reservePub);
|
|
||||||
if (!r) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let amountRaw: AmountJson | undefined = undefined;
|
|
||||||
if (wsr.withdrawalGroupId === r.initialWithdrawalGroupId) {
|
|
||||||
amountRaw = r.instructedAmount;
|
|
||||||
} else {
|
|
||||||
amountRaw = wsr.denomsSel.totalWithdrawCost;
|
|
||||||
}
|
|
||||||
let withdrawalDetails: WithdrawalDetails;
|
let withdrawalDetails: WithdrawalDetails;
|
||||||
if (r.bankInfo) {
|
if (wsr.bankInfo) {
|
||||||
withdrawalDetails = {
|
withdrawalDetails = {
|
||||||
type: WithdrawalType.TalerBankIntegrationApi,
|
type: WithdrawalType.TalerBankIntegrationApi,
|
||||||
confirmed: r.timestampBankConfirmed ? true : false,
|
confirmed: wsr.bankInfo.timestampBankConfirmed ? true : false,
|
||||||
reservePub: wsr.reservePub,
|
reservePub: wsr.reservePub,
|
||||||
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
bankConfirmationUrl: wsr.bankInfo.confirmUrl,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const exchangeDetails = await getExchangeDetails(
|
const exchangeDetails = await getExchangeDetails(
|
||||||
@ -191,7 +177,7 @@ export async function getTransactions(
|
|||||||
transactions.push({
|
transactions.push({
|
||||||
type: TransactionType.Withdrawal,
|
type: TransactionType.Withdrawal,
|
||||||
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
amountEffective: Amounts.stringify(wsr.denomsSel.totalCoinValue),
|
||||||
amountRaw: Amounts.stringify(amountRaw),
|
amountRaw: Amounts.stringify(wsr.rawWithdrawalAmount),
|
||||||
withdrawalDetails,
|
withdrawalDetails,
|
||||||
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
exchangeBaseUrl: wsr.exchangeBaseUrl,
|
||||||
pending: !wsr.timestampFinish,
|
pending: !wsr.timestampFinish,
|
||||||
@ -205,56 +191,6 @@ export async function getTransactions(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Report pending withdrawals based on reserves that
|
|
||||||
// were created, but where the actual withdrawal group has
|
|
||||||
// not started yet.
|
|
||||||
tx.reserves.iter().forEachAsync(async (r) => {
|
|
||||||
if (shouldSkipCurrency(transactionsRequest, r.currency)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (shouldSkipSearch(transactionsRequest, [])) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (r.initialWithdrawalStarted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (r.reserveStatus === ReserveRecordStatus.BankAborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let withdrawalDetails: WithdrawalDetails;
|
|
||||||
if (r.bankInfo) {
|
|
||||||
withdrawalDetails = {
|
|
||||||
type: WithdrawalType.TalerBankIntegrationApi,
|
|
||||||
confirmed: false,
|
|
||||||
reservePub: r.reservePub,
|
|
||||||
bankConfirmationUrl: r.bankInfo.confirmUrl,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
withdrawalDetails = {
|
|
||||||
type: WithdrawalType.ManualTransfer,
|
|
||||||
reservePub: r.reservePub,
|
|
||||||
exchangePaytoUris: await getFundingPaytoUris(tx, r.reservePub),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
transactions.push({
|
|
||||||
type: TransactionType.Withdrawal,
|
|
||||||
amountRaw: Amounts.stringify(r.instructedAmount),
|
|
||||||
amountEffective: Amounts.stringify(
|
|
||||||
r.initialDenomSel.totalCoinValue,
|
|
||||||
),
|
|
||||||
exchangeBaseUrl: r.exchangeBaseUrl,
|
|
||||||
pending: true,
|
|
||||||
timestamp: r.timestampCreated,
|
|
||||||
withdrawalDetails: withdrawalDetails,
|
|
||||||
transactionId: makeEventId(
|
|
||||||
TransactionType.Withdrawal,
|
|
||||||
r.initialWithdrawalGroupId,
|
|
||||||
),
|
|
||||||
frozen: false,
|
|
||||||
...(r.lastError ? { error: r.lastError } : {}),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
tx.depositGroups.iter().forEachAsync(async (dg) => {
|
tx.depositGroups.iter().forEachAsync(async (dg) => {
|
||||||
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
|
const amount = Amounts.parseOrThrow(dg.contractTermsRaw.amount);
|
||||||
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
|
if (shouldSkipCurrency(transactionsRequest, amount.currency)) {
|
||||||
@ -499,7 +435,7 @@ export async function retryTransaction(
|
|||||||
}
|
}
|
||||||
case TransactionType.Withdrawal: {
|
case TransactionType.Withdrawal: {
|
||||||
const withdrawalGroupId = rest[0];
|
const withdrawalGroupId = rest[0];
|
||||||
await processWithdrawGroup(ws, withdrawalGroupId, { forceNow: true });
|
await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TransactionType.Payment: {
|
case TransactionType.Payment: {
|
||||||
@ -536,7 +472,6 @@ export async function deleteTransaction(
|
|||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
reserves: x.reserves,
|
|
||||||
tombstones: x.tombstones,
|
tombstones: x.tombstones,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -550,17 +485,6 @@ export async function deleteTransaction(
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reserveRecord: ReserveRecord | undefined =
|
|
||||||
await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
|
|
||||||
withdrawalGroupId,
|
|
||||||
);
|
|
||||||
if (reserveRecord && !reserveRecord.initialWithdrawalStarted) {
|
|
||||||
const reservePub = reserveRecord.reservePub;
|
|
||||||
await tx.reserves.delete(reservePub);
|
|
||||||
await tx.tombstones.put({
|
|
||||||
id: TombstoneTag.DeleteReserve + ":" + reservePub,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else if (type === TransactionType.Payment) {
|
} else if (type === TransactionType.Payment) {
|
||||||
const proposalId = rest[0];
|
const proposalId = rest[0];
|
||||||
|
@ -19,20 +19,29 @@
|
|||||||
*/
|
*/
|
||||||
import {
|
import {
|
||||||
AbsoluteTime,
|
AbsoluteTime,
|
||||||
|
AcceptManualWithdrawalResult,
|
||||||
|
AcceptWithdrawalResponse,
|
||||||
|
addPaytoQueryParams,
|
||||||
AmountJson,
|
AmountJson,
|
||||||
|
AmountLike,
|
||||||
Amounts,
|
Amounts,
|
||||||
AmountString,
|
AmountString,
|
||||||
BankWithdrawDetails,
|
BankWithdrawDetails,
|
||||||
|
canonicalizeBaseUrl,
|
||||||
|
codecForBankWithdrawalOperationPostResponse,
|
||||||
|
codecForReserveStatus,
|
||||||
codecForTalerConfigResponse,
|
codecForTalerConfigResponse,
|
||||||
codecForWithdrawBatchResponse,
|
codecForWithdrawBatchResponse,
|
||||||
codecForWithdrawOperationStatusResponse,
|
codecForWithdrawOperationStatusResponse,
|
||||||
codecForWithdrawResponse,
|
codecForWithdrawResponse,
|
||||||
DenomKeyType,
|
DenomKeyType,
|
||||||
Duration,
|
Duration,
|
||||||
durationFromSpec,
|
durationFromSpec, encodeCrock,
|
||||||
ExchangeListItem,
|
ExchangeListItem,
|
||||||
ExchangeWithdrawRequest,
|
ExchangeWithdrawRequest,
|
||||||
ForcedDenomSel,
|
ForcedDenomSel,
|
||||||
|
getRandomBytes,
|
||||||
|
j2s,
|
||||||
LibtoolVersion,
|
LibtoolVersion,
|
||||||
Logger,
|
Logger,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
@ -45,8 +54,9 @@ import {
|
|||||||
VersionMatchResult,
|
VersionMatchResult,
|
||||||
WithdrawBatchResponse,
|
WithdrawBatchResponse,
|
||||||
WithdrawResponse,
|
WithdrawResponse,
|
||||||
WithdrawUriInfoResponse,
|
WithdrawUriInfoResponse
|
||||||
} from "@gnu-taler/taler-util";
|
} from "@gnu-taler/taler-util";
|
||||||
|
import { EddsaKeypair } from "../crypto/cryptoImplementation.js";
|
||||||
import {
|
import {
|
||||||
CoinRecord,
|
CoinRecord,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
@ -58,26 +68,42 @@ import {
|
|||||||
ExchangeRecord,
|
ExchangeRecord,
|
||||||
OperationStatus,
|
OperationStatus,
|
||||||
PlanchetRecord,
|
PlanchetRecord,
|
||||||
WithdrawalGroupRecord,
|
ReserveBankInfo,
|
||||||
|
ReserveRecordStatus,
|
||||||
|
WalletStoresV1,
|
||||||
|
WithdrawalGroupRecord
|
||||||
} from "../db.js";
|
} from "../db.js";
|
||||||
import {
|
import {
|
||||||
getErrorDetailFromException,
|
getErrorDetailFromException,
|
||||||
makeErrorDetail,
|
makeErrorDetail,
|
||||||
TalerError,
|
TalerError
|
||||||
} from "../errors.js";
|
} from "../errors.js";
|
||||||
import { InternalWalletState } from "../internal-wallet-state.js";
|
import { InternalWalletState } from "../internal-wallet-state.js";
|
||||||
|
import { assertUnreachable } from "../util/assertUnreachable.js";
|
||||||
import { walletCoreDebugFlags } from "../util/debugFlags.js";
|
import { walletCoreDebugFlags } from "../util/debugFlags.js";
|
||||||
import {
|
import {
|
||||||
HttpRequestLibrary,
|
HttpRequestLibrary,
|
||||||
|
readSuccessResponseJsonOrErrorCode,
|
||||||
readSuccessResponseJsonOrThrow,
|
readSuccessResponseJsonOrThrow,
|
||||||
|
throwUnexpectedRequestError
|
||||||
} from "../util/http.js";
|
} from "../util/http.js";
|
||||||
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants.js";
|
||||||
|
import {
|
||||||
|
DbAccess,
|
||||||
|
GetReadOnlyAccess
|
||||||
|
} from "../util/query.js";
|
||||||
import { RetryInfo } from "../util/retries.js";
|
import { RetryInfo } from "../util/retries.js";
|
||||||
import {
|
import {
|
||||||
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
WALLET_BANK_INTEGRATION_PROTOCOL_VERSION,
|
||||||
WALLET_EXCHANGE_PROTOCOL_VERSION,
|
WALLET_EXCHANGE_PROTOCOL_VERSION
|
||||||
} from "../versions.js";
|
} from "../versions.js";
|
||||||
import { guardOperationException } from "./common.js";
|
import { guardOperationException } from "./common.js";
|
||||||
|
import {
|
||||||
|
getExchangeDetails,
|
||||||
|
getExchangePaytoUri,
|
||||||
|
getExchangeTrust,
|
||||||
|
updateExchangeFromUrl
|
||||||
|
} from "./exchanges.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logger for this file.
|
* Logger for this file.
|
||||||
@ -215,7 +241,7 @@ export function selectWithdrawalDenominations(
|
|||||||
for (const d of denoms) {
|
for (const d of denoms) {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
|
const cost = Amounts.add(d.value, d.feeWithdraw).amount;
|
||||||
for (; ;) {
|
for (;;) {
|
||||||
if (Amounts.cmp(remaining, cost) < 0) {
|
if (Amounts.cmp(remaining, cost) < 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -410,47 +436,42 @@ async function processPlanchetGenerate(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ci = 0;
|
let ci = 0;
|
||||||
let denomPubHash: string | undefined;
|
let maybeDenomPubHash: string | undefined;
|
||||||
for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) {
|
for (let di = 0; di < withdrawalGroup.denomsSel.selectedDenoms.length; di++) {
|
||||||
const d = withdrawalGroup.denomsSel.selectedDenoms[di];
|
const d = withdrawalGroup.denomsSel.selectedDenoms[di];
|
||||||
if (coinIdx >= ci && coinIdx < ci + d.count) {
|
if (coinIdx >= ci && coinIdx < ci + d.count) {
|
||||||
denomPubHash = d.denomPubHash;
|
maybeDenomPubHash = d.denomPubHash;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
ci += d.count;
|
ci += d.count;
|
||||||
}
|
}
|
||||||
if (!denomPubHash) {
|
if (!maybeDenomPubHash) {
|
||||||
throw Error("invariant violated");
|
throw Error("invariant violated");
|
||||||
}
|
}
|
||||||
|
const denomPubHash = maybeDenomPubHash;
|
||||||
|
|
||||||
const { denom, reserve } = await ws.db
|
const denom = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
reserves: x.reserves,
|
|
||||||
denominations: x.denominations,
|
denominations: x.denominations,
|
||||||
}))
|
}))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
const denom = await tx.denominations.get([
|
return ws.getDenomInfo(
|
||||||
|
ws,
|
||||||
|
tx,
|
||||||
withdrawalGroup.exchangeBaseUrl,
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
denomPubHash!,
|
denomPubHash,
|
||||||
]);
|
);
|
||||||
if (!denom) {
|
|
||||||
throw Error("invariant violated");
|
|
||||||
}
|
|
||||||
const reserve = await tx.reserves.get(withdrawalGroup.reservePub);
|
|
||||||
if (!reserve) {
|
|
||||||
throw Error("invariant violated");
|
|
||||||
}
|
|
||||||
return { denom, reserve };
|
|
||||||
});
|
});
|
||||||
|
checkDbInvariant(!!denom);
|
||||||
const r = await ws.cryptoApi.createPlanchet({
|
const r = await ws.cryptoApi.createPlanchet({
|
||||||
denomPub: denom.denomPub,
|
denomPub: denom.denomPub,
|
||||||
feeWithdraw: denom.feeWithdraw,
|
feeWithdraw: denom.feeWithdraw,
|
||||||
reservePriv: reserve.reservePriv,
|
reservePriv: withdrawalGroup.reservePriv,
|
||||||
reservePub: reserve.reservePub,
|
reservePub: withdrawalGroup.reservePub,
|
||||||
value: denom.value,
|
value: denom.value,
|
||||||
coinIndex: coinIdx,
|
coinIndex: coinIdx,
|
||||||
secretSeed: withdrawalGroup.secretSeed,
|
secretSeed: withdrawalGroup.secretSeed,
|
||||||
restrictAge: reserve.restrictAge,
|
restrictAge: withdrawalGroup.restrictAge,
|
||||||
});
|
});
|
||||||
const newPlanchet: PlanchetRecord = {
|
const newPlanchet: PlanchetRecord = {
|
||||||
blindingKey: r.blindingKey,
|
blindingKey: r.blindingKey,
|
||||||
@ -806,11 +827,13 @@ async function processPlanchetVerifyAndStoreCoin(
|
|||||||
|
|
||||||
const planchetCoinPub = planchet.coinPub;
|
const planchetCoinPub = planchet.coinPub;
|
||||||
|
|
||||||
|
// Check if this is the first time that the whole
|
||||||
|
// withdrawal succeeded. If so, mark the withdrawal
|
||||||
|
// group as finished.
|
||||||
const firstSuccess = await ws.db
|
const firstSuccess = await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
coins: x.coins,
|
coins: x.coins,
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
reserves: x.reserves,
|
|
||||||
planchets: x.planchets,
|
planchets: x.planchets,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -875,7 +898,8 @@ export async function updateWithdrawalDenoms(
|
|||||||
denom.verificationStatus === DenominationVerificationStatus.Unverified
|
denom.verificationStatus === DenominationVerificationStatus.Unverified
|
||||||
) {
|
) {
|
||||||
logger.trace(
|
logger.trace(
|
||||||
`Validating denomination (${current + 1}/${denominations.length
|
`Validating denomination (${current + 1}/${
|
||||||
|
denominations.length
|
||||||
}) signature of ${denom.denomPubHash}`,
|
}) signature of ${denom.denomPubHash}`,
|
||||||
);
|
);
|
||||||
let valid = false;
|
let valid = false;
|
||||||
@ -960,7 +984,80 @@ async function reportWithdrawalError(
|
|||||||
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
|
ws.notify({ type: NotificationType.WithdrawOperationError, error: err });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function processWithdrawGroup(
|
/**
|
||||||
|
* Update the information about a reserve that is stored in the wallet
|
||||||
|
* by querying the reserve's exchange.
|
||||||
|
*
|
||||||
|
* If the reserve have funds that are not allocated in a withdrawal group yet
|
||||||
|
* and are big enough to withdraw with available denominations,
|
||||||
|
* create a new withdrawal group for the remaining amount.
|
||||||
|
*/
|
||||||
|
async function queryReserve(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
withdrawalGroupId: string,
|
||||||
|
): Promise<{ ready: boolean }> {
|
||||||
|
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
|
||||||
|
withdrawalGroupId,
|
||||||
|
});
|
||||||
|
checkDbInvariant(!!withdrawalGroup);
|
||||||
|
if (withdrawalGroup.reserveStatus !== ReserveRecordStatus.QueryingStatus) {
|
||||||
|
return { ready: true };
|
||||||
|
}
|
||||||
|
const reservePub = withdrawalGroup.reservePub;
|
||||||
|
|
||||||
|
const reserveUrl = new URL(
|
||||||
|
`reserves/${reservePub}`,
|
||||||
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
reserveUrl.searchParams.set("timeout_ms", "30000");
|
||||||
|
|
||||||
|
logger.info(`querying reserve status via ${reserveUrl}`);
|
||||||
|
|
||||||
|
const resp = await ws.http.get(reserveUrl.href, {
|
||||||
|
timeout: getReserveRequestTimeout(withdrawalGroup),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await readSuccessResponseJsonOrErrorCode(
|
||||||
|
resp,
|
||||||
|
codecForReserveStatus(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.isError) {
|
||||||
|
if (
|
||||||
|
resp.status === 404 &&
|
||||||
|
result.talerErrorResponse.code ===
|
||||||
|
TalerErrorCode.EXCHANGE_RESERVES_STATUS_UNKNOWN
|
||||||
|
) {
|
||||||
|
ws.notify({
|
||||||
|
type: NotificationType.ReserveNotYetFound,
|
||||||
|
reservePub,
|
||||||
|
});
|
||||||
|
return { ready: false };
|
||||||
|
} else {
|
||||||
|
throwUnexpectedRequestError(resp, result.talerErrorResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.trace(`got reserve status ${j2s(result.response)}`);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const wg = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
if (!wg) {
|
||||||
|
logger.warn(`withdrawal group ${withdrawalGroupId} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
wg.reserveStatus = ReserveRecordStatus.Dormant;
|
||||||
|
await tx.withdrawalGroups.put(wg);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { ready: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processWithdrawalGroup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
withdrawalGroupId: string,
|
withdrawalGroupId: string,
|
||||||
options: {
|
options: {
|
||||||
@ -990,24 +1087,42 @@ async function processWithdrawGroupImpl(
|
|||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
return tx.withdrawalGroups.get(withdrawalGroupId);
|
return tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!withdrawalGroup) {
|
if (!withdrawalGroup) {
|
||||||
// Withdrawal group doesn't exist yet, but reserve might exist
|
throw Error(`withdrawal group ${withdrawalGroupId} not found`);
|
||||||
// (and reference the yet to be created withdrawal group)
|
}
|
||||||
const reservePub = await ws.db
|
|
||||||
.mktx((x) => ({ reserves: x.reserves }))
|
switch (withdrawalGroup.reserveStatus) {
|
||||||
.runReadOnly(async (tx) => {
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
const r = await tx.reserves.indexes.byInitialWithdrawalGroupId.get(
|
await processReserveBankStatus(ws, withdrawalGroupId);
|
||||||
withdrawalGroupId,
|
return await processWithdrawGroupImpl(ws, withdrawalGroupId, {
|
||||||
);
|
forceNow: true,
|
||||||
return r?.reservePub;
|
|
||||||
});
|
});
|
||||||
if (!reservePub) {
|
case ReserveRecordStatus.QueryingStatus: {
|
||||||
logger.warn(
|
const res = await queryReserve(ws, withdrawalGroupId);
|
||||||
"withdrawal group doesn't exist (and reserve doesn't exist either)",
|
if (res.ready) {
|
||||||
);
|
return await processWithdrawGroupImpl(ws, withdrawalGroupId, {
|
||||||
|
forceNow: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return await ws.reserveOps.processReserve(ws, reservePub, { forceNow });
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
|
await processReserveBankStatus(ws, withdrawalGroupId);
|
||||||
|
return;
|
||||||
|
case ReserveRecordStatus.BankAborted:
|
||||||
|
// FIXME
|
||||||
|
return;
|
||||||
|
case ReserveRecordStatus.Dormant:
|
||||||
|
// We can try to withdraw, nothing needs to be done with the reserve.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
logger.warn(
|
||||||
|
"unknown reserve record status:",
|
||||||
|
withdrawalGroup.reserveStatus,
|
||||||
|
);
|
||||||
|
assertUnreachable(withdrawalGroup.reserveStatus);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ws.exchangeOps.updateExchangeFromUrl(
|
await ws.exchangeOps.updateExchangeFromUrl(
|
||||||
@ -1071,7 +1186,6 @@ async function processWithdrawGroupImpl(
|
|||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
coins: x.coins,
|
coins: x.coins,
|
||||||
withdrawalGroups: x.withdrawalGroups,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
reserves: x.reserves,
|
|
||||||
planchets: x.planchets,
|
planchets: x.planchets,
|
||||||
}))
|
}))
|
||||||
.runReadWrite(async (tx) => {
|
.runReadWrite(async (tx) => {
|
||||||
@ -1200,9 +1314,9 @@ export async function getExchangeWithdrawalInfo(
|
|||||||
!versionMatch.compatible &&
|
!versionMatch.compatible &&
|
||||||
versionMatch.currentCmp === -1
|
versionMatch.currentCmp === -1
|
||||||
) {
|
) {
|
||||||
console.warn(
|
logger.warn(
|
||||||
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
|
`wallet's support for exchange protocol version ${WALLET_EXCHANGE_PROTOCOL_VERSION} might be outdated ` +
|
||||||
`(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
|
`(exchange has ${exchangeDetails.protocolVersion}), checking for updates`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1308,3 +1422,456 @@ export async function getWithdrawalDetailsForUri(
|
|||||||
possibleExchanges: exchanges,
|
possibleExchanges: exchanges,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getFundingPaytoUrisTx(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
withdrawalGroupId: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
return await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite((tx) => getFundingPaytoUris(tx, withdrawalGroupId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get payto URIs that can be used to fund a withdrawal operation.
|
||||||
|
*/
|
||||||
|
export async function getFundingPaytoUris(
|
||||||
|
tx: GetReadOnlyAccess<{
|
||||||
|
withdrawalGroups: typeof WalletStoresV1.withdrawalGroups;
|
||||||
|
exchanges: typeof WalletStoresV1.exchanges;
|
||||||
|
exchangeDetails: typeof WalletStoresV1.exchangeDetails;
|
||||||
|
}>,
|
||||||
|
withdrawalGroupId: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const withdrawalGroup = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
checkDbInvariant(!!withdrawalGroup);
|
||||||
|
const exchangeDetails = await getExchangeDetails(
|
||||||
|
tx,
|
||||||
|
withdrawalGroup.exchangeBaseUrl,
|
||||||
|
);
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
logger.error(`exchange ${withdrawalGroup.exchangeBaseUrl} not found`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const plainPaytoUris =
|
||||||
|
exchangeDetails.wireInfo?.accounts.map((x) => x.payto_uri) ?? [];
|
||||||
|
if (!plainPaytoUris) {
|
||||||
|
logger.error(
|
||||||
|
`exchange ${withdrawalGroup.exchangeBaseUrl} has no wire info`,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return plainPaytoUris.map((x) =>
|
||||||
|
addPaytoQueryParams(x, {
|
||||||
|
amount: Amounts.stringify(withdrawalGroup.instructedAmount),
|
||||||
|
message: `Taler Withdrawal ${withdrawalGroup.reservePub}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWithdrawalGroupRecordTx(
|
||||||
|
db: DbAccess<typeof WalletStoresV1>,
|
||||||
|
req: {
|
||||||
|
withdrawalGroupId: string;
|
||||||
|
},
|
||||||
|
): Promise<WithdrawalGroupRecord | undefined> {
|
||||||
|
return await db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return tx.withdrawalGroups.get(req.withdrawalGroupId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getReserveRequestTimeout(r: WithdrawalGroupRecord): Duration {
|
||||||
|
return Duration.max(
|
||||||
|
{ d_ms: 60000 },
|
||||||
|
Duration.min({ d_ms: 5000 }, RetryInfo.getDuration(r.retryInfo)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function registerReserveWithBank(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
withdrawalGroupId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const withdrawalGroup = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadOnly(async (tx) => {
|
||||||
|
return await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
});
|
||||||
|
switch (withdrawalGroup?.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bankInfo = withdrawalGroup.bankInfo;
|
||||||
|
if (!bankInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bankStatusUrl = bankInfo.statusUrl;
|
||||||
|
const httpResp = await ws.http.postJson(
|
||||||
|
bankStatusUrl,
|
||||||
|
{
|
||||||
|
reserve_pub: withdrawalGroup.reservePub,
|
||||||
|
selected_exchange: bankInfo.exchangePaytoUri,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timeout: getReserveRequestTimeout(withdrawalGroup),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await readSuccessResponseJsonOrThrow(
|
||||||
|
httpResp,
|
||||||
|
codecForBankWithdrawalOperationPostResponse(),
|
||||||
|
);
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (r.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.bankInfo) {
|
||||||
|
throw Error("invariant failed");
|
||||||
|
}
|
||||||
|
r.bankInfo.timestampReserveInfoPosted = AbsoluteTime.toTimestamp(
|
||||||
|
AbsoluteTime.now(),
|
||||||
|
);
|
||||||
|
r.reserveStatus = ReserveRecordStatus.WaitConfirmBank;
|
||||||
|
r.operationStatus = OperationStatus.Pending;
|
||||||
|
r.retryInfo = RetryInfo.reset();
|
||||||
|
await tx.withdrawalGroups.put(r);
|
||||||
|
});
|
||||||
|
ws.notify({ type: NotificationType.ReserveRegisteredWithBank });
|
||||||
|
return processReserveBankStatus(ws, withdrawalGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function processReserveBankStatus(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
withdrawalGroupId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const withdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
|
||||||
|
withdrawalGroupId,
|
||||||
|
});
|
||||||
|
switch (withdrawalGroup?.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const bankStatusUrl = withdrawalGroup.bankInfo?.statusUrl;
|
||||||
|
if (!bankStatusUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusResp = await ws.http.get(bankStatusUrl, {
|
||||||
|
timeout: getReserveRequestTimeout(withdrawalGroup),
|
||||||
|
});
|
||||||
|
const status = await readSuccessResponseJsonOrThrow(
|
||||||
|
statusResp,
|
||||||
|
codecForWithdrawOperationStatusResponse(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status.aborted) {
|
||||||
|
logger.info("bank aborted the withdrawal");
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (r.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!r.bankInfo) {
|
||||||
|
throw Error("invariant failed");
|
||||||
|
}
|
||||||
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
||||||
|
r.bankInfo.timestampBankConfirmed = now;
|
||||||
|
r.reserveStatus = ReserveRecordStatus.BankAborted;
|
||||||
|
r.operationStatus = OperationStatus.Finished;
|
||||||
|
r.retryInfo = RetryInfo.reset();
|
||||||
|
await tx.withdrawalGroups.put(r);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bank still needs to know our reserve info
|
||||||
|
if (!status.selection_done) {
|
||||||
|
await registerReserveWithBank(ws, withdrawalGroupId);
|
||||||
|
return await processReserveBankStatus(ws, withdrawalGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Why do we do this?!
|
||||||
|
if (withdrawalGroup.reserveStatus === ReserveRecordStatus.RegisteringBank) {
|
||||||
|
await registerReserveWithBank(ws, withdrawalGroupId);
|
||||||
|
return await processReserveBankStatus(ws, withdrawalGroupId);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
const r = await tx.withdrawalGroups.get(withdrawalGroupId);
|
||||||
|
if (!r) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Re-check reserve status within transaction
|
||||||
|
switch (r.reserveStatus) {
|
||||||
|
case ReserveRecordStatus.RegisteringBank:
|
||||||
|
case ReserveRecordStatus.WaitConfirmBank:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status.transfer_done) {
|
||||||
|
logger.info("withdrawal: transfer confirmed by bank.");
|
||||||
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
||||||
|
if (!r.bankInfo) {
|
||||||
|
throw Error("invariant failed");
|
||||||
|
}
|
||||||
|
r.bankInfo.timestampBankConfirmed = now;
|
||||||
|
r.reserveStatus = ReserveRecordStatus.QueryingStatus;
|
||||||
|
r.operationStatus = OperationStatus.Pending;
|
||||||
|
r.retryInfo = RetryInfo.reset();
|
||||||
|
} else {
|
||||||
|
logger.info("withdrawal: transfer not yet confirmed by bank");
|
||||||
|
if (r.bankInfo) {
|
||||||
|
r.bankInfo.confirmUrl = status.confirm_transfer_url;
|
||||||
|
}
|
||||||
|
r.retryInfo = RetryInfo.increment(r.retryInfo);
|
||||||
|
}
|
||||||
|
await tx.withdrawalGroups.put(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function internalCreateWithdrawalGroup(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
args: {
|
||||||
|
reserveStatus: ReserveRecordStatus;
|
||||||
|
amount: AmountJson;
|
||||||
|
bankInfo?: ReserveBankInfo;
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
forcedDenomSel?: ForcedDenomSel;
|
||||||
|
reserveKeyPair?: EddsaKeypair;
|
||||||
|
restrictAge?: number;
|
||||||
|
},
|
||||||
|
): Promise<WithdrawalGroupRecord> {
|
||||||
|
const reserveKeyPair =
|
||||||
|
args.reserveKeyPair ?? (await ws.cryptoApi.createEddsaKeypair({}));
|
||||||
|
const now = AbsoluteTime.toTimestamp(AbsoluteTime.now());
|
||||||
|
const secretSeed = encodeCrock(getRandomBytes(32));
|
||||||
|
const canonExchange = canonicalizeBaseUrl(args.exchangeBaseUrl);
|
||||||
|
const withdrawalGroupId = encodeCrock(getRandomBytes(32));
|
||||||
|
const amount = args.amount;
|
||||||
|
|
||||||
|
await updateWithdrawalDenoms(ws, canonExchange);
|
||||||
|
const denoms = await getCandidateWithdrawalDenoms(ws, canonExchange);
|
||||||
|
|
||||||
|
let initialDenomSel: DenomSelectionState;
|
||||||
|
const denomSelUid = encodeCrock(getRandomBytes(16));
|
||||||
|
if (args.forcedDenomSel) {
|
||||||
|
logger.warn("using forced denom selection");
|
||||||
|
initialDenomSel = selectForcedWithdrawalDenominations(
|
||||||
|
amount,
|
||||||
|
denoms,
|
||||||
|
args.forcedDenomSel,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
initialDenomSel = selectWithdrawalDenominations(amount, denoms);
|
||||||
|
}
|
||||||
|
|
||||||
|
const withdrawalGroup: WithdrawalGroupRecord = {
|
||||||
|
denomSelUid,
|
||||||
|
denomsSel: initialDenomSel,
|
||||||
|
exchangeBaseUrl: canonExchange,
|
||||||
|
instructedAmount: amount,
|
||||||
|
timestampStart: now,
|
||||||
|
lastError: undefined,
|
||||||
|
operationStatus: OperationStatus.Pending,
|
||||||
|
rawWithdrawalAmount: initialDenomSel.totalWithdrawCost,
|
||||||
|
secretSeed,
|
||||||
|
reservePriv: reserveKeyPair.priv,
|
||||||
|
reservePub: reserveKeyPair.pub,
|
||||||
|
reserveStatus: args.reserveStatus,
|
||||||
|
retryInfo: RetryInfo.reset(),
|
||||||
|
withdrawalGroupId,
|
||||||
|
bankInfo: args.bankInfo,
|
||||||
|
restrictAge: args.restrictAge,
|
||||||
|
senderWire: undefined,
|
||||||
|
timestampFinish: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const exchangeInfo = await updateExchangeFromUrl(ws, canonExchange);
|
||||||
|
const exchangeDetails = exchangeInfo.exchangeDetails;
|
||||||
|
if (!exchangeDetails) {
|
||||||
|
logger.trace(exchangeDetails);
|
||||||
|
throw Error("exchange not updated");
|
||||||
|
}
|
||||||
|
const { isAudited, isTrusted } = await getExchangeTrust(
|
||||||
|
ws,
|
||||||
|
exchangeInfo.exchange,
|
||||||
|
);
|
||||||
|
|
||||||
|
await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
exchangeTrust: x.exchangeTrust,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
await tx.withdrawalGroups.add(withdrawalGroup);
|
||||||
|
|
||||||
|
if (!isAudited && !isTrusted) {
|
||||||
|
await tx.exchangeTrust.put({
|
||||||
|
currency: amount.currency,
|
||||||
|
exchangeBaseUrl: canonExchange,
|
||||||
|
exchangeMasterPub: exchangeDetails.masterPublicKey,
|
||||||
|
uids: [encodeCrock(getRandomBytes(32))],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return withdrawalGroup;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function acceptWithdrawalFromUri(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: {
|
||||||
|
talerWithdrawUri: string;
|
||||||
|
selectedExchange: string;
|
||||||
|
forcedDenomSel?: ForcedDenomSel;
|
||||||
|
restrictAge?: number;
|
||||||
|
},
|
||||||
|
): Promise<AcceptWithdrawalResponse> {
|
||||||
|
await updateExchangeFromUrl(ws, req.selectedExchange);
|
||||||
|
const withdrawInfo = await getBankWithdrawalInfo(
|
||||||
|
ws.http,
|
||||||
|
req.talerWithdrawUri,
|
||||||
|
);
|
||||||
|
const exchangePaytoUri = await getExchangePaytoUri(
|
||||||
|
ws,
|
||||||
|
req.selectedExchange,
|
||||||
|
withdrawInfo.wireTypes,
|
||||||
|
);
|
||||||
|
|
||||||
|
const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
|
||||||
|
amount: withdrawInfo.amount,
|
||||||
|
exchangeBaseUrl: req.selectedExchange,
|
||||||
|
forcedDenomSel: req.forcedDenomSel,
|
||||||
|
reserveStatus: ReserveRecordStatus.RegisteringBank,
|
||||||
|
bankInfo: {
|
||||||
|
exchangePaytoUri,
|
||||||
|
statusUrl: withdrawInfo.extractedStatusUrl,
|
||||||
|
confirmUrl: withdrawInfo.confirmTransferUrl,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
|
||||||
|
|
||||||
|
// We do this here, as the reserve should be registered before we return,
|
||||||
|
// so that we can redirect the user to the bank's status page.
|
||||||
|
await processReserveBankStatus(ws, withdrawalGroupId);
|
||||||
|
const processedWithdrawalGroup = await getWithdrawalGroupRecordTx(ws.db, {
|
||||||
|
withdrawalGroupId,
|
||||||
|
});
|
||||||
|
if (
|
||||||
|
processedWithdrawalGroup?.reserveStatus === ReserveRecordStatus.BankAborted
|
||||||
|
) {
|
||||||
|
throw TalerError.fromDetail(
|
||||||
|
TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start withdrawal in the background.
|
||||||
|
await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true }).catch(
|
||||||
|
(err) => {
|
||||||
|
logger.error("Processing withdrawal (after creation) failed:", err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reservePub: withdrawalGroup.reservePub,
|
||||||
|
confirmTransferUrl: withdrawInfo.confirmTransferUrl,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a manual withdrawal operation.
|
||||||
|
*
|
||||||
|
* Adds the corresponding exchange as a trusted exchange if it is neither
|
||||||
|
* audited nor trusted already.
|
||||||
|
*
|
||||||
|
* Asynchronously starts the withdrawal.
|
||||||
|
*/
|
||||||
|
export async function createManualWithdrawal(
|
||||||
|
ws: InternalWalletState,
|
||||||
|
req: {
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
amount: AmountLike;
|
||||||
|
restrictAge?: number;
|
||||||
|
forcedDenomSel?: ForcedDenomSel;
|
||||||
|
},
|
||||||
|
): Promise<AcceptManualWithdrawalResult> {
|
||||||
|
const withdrawalGroup = await internalCreateWithdrawalGroup(ws, {
|
||||||
|
amount: Amounts.jsonifyAmount(req.amount),
|
||||||
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
|
bankInfo: undefined,
|
||||||
|
forcedDenomSel: req.forcedDenomSel,
|
||||||
|
restrictAge: req.restrictAge,
|
||||||
|
reserveStatus: ReserveRecordStatus.QueryingStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
const withdrawalGroupId = withdrawalGroup.withdrawalGroupId;
|
||||||
|
|
||||||
|
const exchangePaytoUris = await ws.db
|
||||||
|
.mktx((x) => ({
|
||||||
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
|
exchanges: x.exchanges,
|
||||||
|
exchangeDetails: x.exchangeDetails,
|
||||||
|
exchangeTrust: x.exchangeTrust,
|
||||||
|
}))
|
||||||
|
.runReadWrite(async (tx) => {
|
||||||
|
return await getFundingPaytoUris(tx, withdrawalGroup.withdrawalGroupId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start withdrawal in the background.
|
||||||
|
await processWithdrawalGroup(ws, withdrawalGroupId, { forceNow: true }).catch(
|
||||||
|
(err) => {
|
||||||
|
logger.error("Processing withdrawal (after creation) failed:", err);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
reservePub: withdrawalGroup.reservePub,
|
||||||
|
exchangePaytoUris: exchangePaytoUris,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -40,7 +40,6 @@ export enum PendingTaskType {
|
|||||||
ProposalChoice = "proposal-choice",
|
ProposalChoice = "proposal-choice",
|
||||||
ProposalDownload = "proposal-download",
|
ProposalDownload = "proposal-download",
|
||||||
Refresh = "refresh",
|
Refresh = "refresh",
|
||||||
Reserve = "reserve",
|
|
||||||
Recoup = "recoup",
|
Recoup = "recoup",
|
||||||
RefundQuery = "refund-query",
|
RefundQuery = "refund-query",
|
||||||
TipPickup = "tip-pickup",
|
TipPickup = "tip-pickup",
|
||||||
@ -60,7 +59,6 @@ export type PendingTaskInfo = PendingTaskInfoCommon &
|
|||||||
| PendingProposalDownloadTask
|
| PendingProposalDownloadTask
|
||||||
| PendingRefreshTask
|
| PendingRefreshTask
|
||||||
| PendingRefundQueryTask
|
| PendingRefundQueryTask
|
||||||
| PendingReserveTask
|
|
||||||
| PendingTipPickupTask
|
| PendingTipPickupTask
|
||||||
| PendingWithdrawTask
|
| PendingWithdrawTask
|
||||||
| PendingRecoupTask
|
| PendingRecoupTask
|
||||||
@ -103,22 +101,6 @@ export enum ReserveType {
|
|||||||
TalerBankWithdraw = "taler-bank-withdraw",
|
TalerBankWithdraw = "taler-bank-withdraw",
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Status of processing a reserve.
|
|
||||||
*
|
|
||||||
* Does *not* include the withdrawal operation that might result
|
|
||||||
* from this.
|
|
||||||
*/
|
|
||||||
export interface PendingReserveTask {
|
|
||||||
type: PendingTaskType.Reserve;
|
|
||||||
retryInfo: RetryInfo | undefined;
|
|
||||||
stage: ReserveRecordStatus;
|
|
||||||
timestampCreated: TalerProtocolTimestamp;
|
|
||||||
reserveType: ReserveType;
|
|
||||||
reservePub: string;
|
|
||||||
bankWithdrawConfirmUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Status of an ongoing withdrawal operation.
|
* Status of an ongoing withdrawal operation.
|
||||||
*/
|
*/
|
||||||
|
@ -107,7 +107,6 @@ import {
|
|||||||
MerchantOperations,
|
MerchantOperations,
|
||||||
NotificationListener,
|
NotificationListener,
|
||||||
RecoupOperations,
|
RecoupOperations,
|
||||||
ReserveOperations,
|
|
||||||
} from "./internal-wallet-state.js";
|
} from "./internal-wallet-state.js";
|
||||||
import { exportBackup } from "./operations/backup/export.js";
|
import { exportBackup } from "./operations/backup/export.js";
|
||||||
import {
|
import {
|
||||||
@ -167,12 +166,6 @@ import {
|
|||||||
prepareRefund,
|
prepareRefund,
|
||||||
processPurchaseQueryRefund,
|
processPurchaseQueryRefund,
|
||||||
} from "./operations/refund.js";
|
} from "./operations/refund.js";
|
||||||
import {
|
|
||||||
createReserve,
|
|
||||||
createTalerWithdrawReserve,
|
|
||||||
getFundingPaytoUris,
|
|
||||||
processReserve,
|
|
||||||
} from "./operations/reserves.js";
|
|
||||||
import {
|
import {
|
||||||
runIntegrationTest,
|
runIntegrationTest,
|
||||||
testPay,
|
testPay,
|
||||||
@ -185,9 +178,12 @@ import {
|
|||||||
retryTransaction,
|
retryTransaction,
|
||||||
} from "./operations/transactions.js";
|
} from "./operations/transactions.js";
|
||||||
import {
|
import {
|
||||||
|
acceptWithdrawalFromUri,
|
||||||
|
createManualWithdrawal,
|
||||||
getExchangeWithdrawalInfo,
|
getExchangeWithdrawalInfo,
|
||||||
|
getFundingPaytoUrisTx,
|
||||||
getWithdrawalDetailsForUri,
|
getWithdrawalDetailsForUri,
|
||||||
processWithdrawGroup,
|
processWithdrawalGroup as processWithdrawalGroup,
|
||||||
} from "./operations/withdraw.js";
|
} from "./operations/withdraw.js";
|
||||||
import {
|
import {
|
||||||
PendingOperationsResponse,
|
PendingOperationsResponse,
|
||||||
@ -258,11 +254,8 @@ async function processOnePendingOperation(
|
|||||||
case PendingTaskType.Refresh:
|
case PendingTaskType.Refresh:
|
||||||
await processRefreshGroup(ws, pending.refreshGroupId, { forceNow });
|
await processRefreshGroup(ws, pending.refreshGroupId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.Reserve:
|
|
||||||
await processReserve(ws, pending.reservePub, { forceNow });
|
|
||||||
break;
|
|
||||||
case PendingTaskType.Withdraw:
|
case PendingTaskType.Withdraw:
|
||||||
await processWithdrawGroup(ws, pending.withdrawalGroupId, { forceNow });
|
await processWithdrawalGroup(ws, pending.withdrawalGroupId, { forceNow });
|
||||||
break;
|
break;
|
||||||
case PendingTaskType.ProposalDownload:
|
case PendingTaskType.ProposalDownload:
|
||||||
await processDownloadProposal(ws, pending.proposalId, { forceNow });
|
await processDownloadProposal(ws, pending.proposalId, { forceNow });
|
||||||
@ -464,40 +457,6 @@ async function fillDefaults(ws: InternalWalletState): Promise<void> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a reserve for a manual withdrawal.
|
|
||||||
*
|
|
||||||
* Adds the corresponding exchange as a trusted exchange if it is neither
|
|
||||||
* audited nor trusted already.
|
|
||||||
*/
|
|
||||||
async function acceptManualWithdrawal(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
exchangeBaseUrl: string,
|
|
||||||
amount: AmountJson,
|
|
||||||
restrictAge?: number,
|
|
||||||
): Promise<AcceptManualWithdrawalResult> {
|
|
||||||
try {
|
|
||||||
const resp = await createReserve(ws, {
|
|
||||||
amount,
|
|
||||||
exchange: exchangeBaseUrl,
|
|
||||||
restrictAge,
|
|
||||||
});
|
|
||||||
const exchangePaytoUris = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
exchanges: x.exchanges,
|
|
||||||
exchangeDetails: x.exchangeDetails,
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadWrite((tx) => getFundingPaytoUris(tx, resp.reservePub));
|
|
||||||
return {
|
|
||||||
reservePub: resp.reservePub,
|
|
||||||
exchangePaytoUris,
|
|
||||||
};
|
|
||||||
} finally {
|
|
||||||
ws.latch.trigger();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getExchangeTos(
|
async function getExchangeTos(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
exchangeBaseUrl: string,
|
exchangeBaseUrl: string,
|
||||||
@ -552,6 +511,10 @@ async function getExchangeTos(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List bank accounts known to the wallet from
|
||||||
|
* previous withdrawals.
|
||||||
|
*/
|
||||||
async function listKnownBankAccounts(
|
async function listKnownBankAccounts(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
currency?: string,
|
currency?: string,
|
||||||
@ -559,12 +522,13 @@ async function listKnownBankAccounts(
|
|||||||
const accounts: PaytoUri[] = [];
|
const accounts: PaytoUri[] = [];
|
||||||
await ws.db
|
await ws.db
|
||||||
.mktx((x) => ({
|
.mktx((x) => ({
|
||||||
reserves: x.reserves,
|
withdrawalGroups: x.withdrawalGroups,
|
||||||
}))
|
}))
|
||||||
.runReadOnly(async (tx) => {
|
.runReadOnly(async (tx) => {
|
||||||
const reservesRecords = await tx.reserves.iter().toArray();
|
const withdrawalGroups = await tx.withdrawalGroups.iter().toArray();
|
||||||
for (const r of reservesRecords) {
|
for (const r of withdrawalGroups) {
|
||||||
if (currency && currency !== r.currency) {
|
const amount = r.rawWithdrawalAmount;
|
||||||
|
if (currency && currency !== amount.currency) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const payto = r.senderWire ? parsePaytoUri(r.senderWire) : undefined;
|
const payto = r.senderWire ? parsePaytoUri(r.senderWire) : undefined;
|
||||||
@ -614,31 +578,6 @@ async function getExchanges(
|
|||||||
return { exchanges };
|
return { exchanges };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inform the wallet that the status of a reserve has changed (e.g. due to a
|
|
||||||
* confirmation from the bank.).
|
|
||||||
*/
|
|
||||||
export async function handleNotifyReserve(
|
|
||||||
ws: InternalWalletState,
|
|
||||||
): Promise<void> {
|
|
||||||
const reserves = await ws.db
|
|
||||||
.mktx((x) => ({
|
|
||||||
reserves: x.reserves,
|
|
||||||
}))
|
|
||||||
.runReadOnly(async (tx) => {
|
|
||||||
return tx.reserves.iter().toArray();
|
|
||||||
});
|
|
||||||
for (const r of reserves) {
|
|
||||||
if (r.reserveStatus === ReserveRecordStatus.WaitConfirmBank) {
|
|
||||||
try {
|
|
||||||
processReserve(ws, r.reservePub);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setCoinSuspended(
|
async function setCoinSuspended(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
coinPub: string,
|
coinPub: string,
|
||||||
@ -817,12 +756,11 @@ async function dispatchRequestInternal(
|
|||||||
}
|
}
|
||||||
case "acceptManualWithdrawal": {
|
case "acceptManualWithdrawal": {
|
||||||
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
|
const req = codecForAcceptManualWithdrawalRequet().decode(payload);
|
||||||
const res = await acceptManualWithdrawal(
|
const res = await createManualWithdrawal(ws, {
|
||||||
ws,
|
amount: Amounts.parseOrThrow(req.amount),
|
||||||
req.exchangeBaseUrl,
|
exchangeBaseUrl: req.exchangeBaseUrl,
|
||||||
Amounts.parseOrThrow(req.amount),
|
restrictAge: req.restrictAge,
|
||||||
req.restrictAge,
|
});
|
||||||
);
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
case "getWithdrawalDetailsForAmount": {
|
case "getWithdrawalDetailsForAmount": {
|
||||||
@ -856,15 +794,12 @@ async function dispatchRequestInternal(
|
|||||||
case "acceptBankIntegratedWithdrawal": {
|
case "acceptBankIntegratedWithdrawal": {
|
||||||
const req =
|
const req =
|
||||||
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
|
codecForAcceptBankIntegratedWithdrawalRequest().decode(payload);
|
||||||
return await createTalerWithdrawReserve(
|
return await acceptWithdrawalFromUri(ws, {
|
||||||
ws,
|
selectedExchange: req.exchangeBaseUrl,
|
||||||
req.talerWithdrawUri,
|
talerWithdrawUri: req.talerWithdrawUri,
|
||||||
req.exchangeBaseUrl,
|
forcedDenomSel: req.forcedDenomSel,
|
||||||
{
|
restrictAge: req.restrictAge,
|
||||||
forcedDenomSel: req.forcedDenomSel,
|
});
|
||||||
restrictAge: req.restrictAge,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
case "getExchangeTos": {
|
case "getExchangeTos": {
|
||||||
const req = codecForGetExchangeTosRequest().decode(payload);
|
const req = codecForGetExchangeTosRequest().decode(payload);
|
||||||
@ -1033,7 +968,10 @@ async function dispatchRequestInternal(
|
|||||||
req.exchange,
|
req.exchange,
|
||||||
amount,
|
amount,
|
||||||
);
|
);
|
||||||
const wres = await acceptManualWithdrawal(ws, req.exchange, amount);
|
const wres = await createManualWithdrawal(ws, {
|
||||||
|
amount: amount,
|
||||||
|
exchangeBaseUrl: req.exchange,
|
||||||
|
});
|
||||||
const paytoUri = details.paytoUris[0];
|
const paytoUri = details.paytoUris[0];
|
||||||
const pt = parsePaytoUri(paytoUri);
|
const pt = parsePaytoUri(paytoUri);
|
||||||
if (!pt) {
|
if (!pt) {
|
||||||
@ -1229,10 +1167,6 @@ class InternalWalletStateImpl implements InternalWalletState {
|
|||||||
getMerchantInfo,
|
getMerchantInfo,
|
||||||
};
|
};
|
||||||
|
|
||||||
reserveOps: ReserveOperations = {
|
|
||||||
processReserve,
|
|
||||||
};
|
|
||||||
|
|
||||||
// FIXME: Use an LRU cache here.
|
// FIXME: Use an LRU cache here.
|
||||||
private denomCache: Record<string, DenomInfo> = {};
|
private denomCache: Record<string, DenomInfo> = {};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user