backup import WIP
This commit is contained in:
parent
84d5b5e5ef
commit
95568395ce
@ -31,6 +31,7 @@ import {
|
|||||||
BackupCoinSource,
|
BackupCoinSource,
|
||||||
BackupCoinSourceType,
|
BackupCoinSourceType,
|
||||||
BackupDenomination,
|
BackupDenomination,
|
||||||
|
BackupDenomSel,
|
||||||
BackupExchange,
|
BackupExchange,
|
||||||
BackupExchangeWireFee,
|
BackupExchangeWireFee,
|
||||||
BackupProposal,
|
BackupProposal,
|
||||||
@ -39,6 +40,7 @@ import {
|
|||||||
BackupRecoupGroup,
|
BackupRecoupGroup,
|
||||||
BackupRefreshGroup,
|
BackupRefreshGroup,
|
||||||
BackupRefreshOldCoin,
|
BackupRefreshOldCoin,
|
||||||
|
BackupRefreshReason,
|
||||||
BackupRefreshSession,
|
BackupRefreshSession,
|
||||||
BackupRefundItem,
|
BackupRefundItem,
|
||||||
BackupRefundState,
|
BackupRefundState,
|
||||||
@ -50,15 +52,24 @@ import {
|
|||||||
import { TransactionHandle } from "../util/query";
|
import { TransactionHandle } from "../util/query";
|
||||||
import {
|
import {
|
||||||
AbortStatus,
|
AbortStatus,
|
||||||
|
CoinSource,
|
||||||
CoinSourceType,
|
CoinSourceType,
|
||||||
CoinStatus,
|
CoinStatus,
|
||||||
ConfigRecord,
|
ConfigRecord,
|
||||||
|
DenominationStatus,
|
||||||
|
DenomSelectionState,
|
||||||
|
ExchangeUpdateStatus,
|
||||||
|
ExchangeWireInfo,
|
||||||
|
ProposalDownload,
|
||||||
ProposalStatus,
|
ProposalStatus,
|
||||||
|
RefreshSessionRecord,
|
||||||
RefundState,
|
RefundState,
|
||||||
|
ReserveBankInfo,
|
||||||
|
ReserveRecordStatus,
|
||||||
Stores,
|
Stores,
|
||||||
} from "../types/dbTypes";
|
} from "../types/dbTypes";
|
||||||
import { checkDbInvariant } from "../util/invariants";
|
import { checkDbInvariant, checkLogicInvariant } from "../util/invariants";
|
||||||
import { Amounts, codecForAmountString } from "../util/amounts";
|
import { AmountJson, Amounts, codecForAmountString } from "../util/amounts";
|
||||||
import {
|
import {
|
||||||
decodeCrock,
|
decodeCrock,
|
||||||
eddsaGetPublic,
|
eddsaGetPublic,
|
||||||
@ -71,7 +82,11 @@ import {
|
|||||||
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
|
import { canonicalizeBaseUrl, canonicalJson, j2s } from "../util/helpers";
|
||||||
import { getTimestampNow, Timestamp } from "../util/time";
|
import { getTimestampNow, Timestamp } from "../util/time";
|
||||||
import { URL } from "../util/url";
|
import { URL } from "../util/url";
|
||||||
import { AmountString, TipResponse } from "../types/talerTypes";
|
import {
|
||||||
|
AmountString,
|
||||||
|
codecForContractTerms,
|
||||||
|
ContractTerms,
|
||||||
|
} from "../types/talerTypes";
|
||||||
import {
|
import {
|
||||||
buildCodecForObject,
|
buildCodecForObject,
|
||||||
Codec,
|
Codec,
|
||||||
@ -85,6 +100,8 @@ import {
|
|||||||
import { Logger } from "../util/logging";
|
import { Logger } from "../util/logging";
|
||||||
import { gzipSync } from "fflate";
|
import { gzipSync } from "fflate";
|
||||||
import { kdf } from "../crypto/primitives/kdf";
|
import { kdf } from "../crypto/primitives/kdf";
|
||||||
|
import { initRetryInfo } from "../util/retries";
|
||||||
|
import { RefreshReason } from "../types/walletTypes";
|
||||||
|
|
||||||
interface WalletBackupConfState {
|
interface WalletBackupConfState {
|
||||||
deviceId: string;
|
deviceId: string;
|
||||||
@ -207,7 +224,7 @@ export async function exportBackup(
|
|||||||
timestamp_start: wg.timestampStart,
|
timestamp_start: wg.timestampStart,
|
||||||
timestamp_finish: wg.timestampFinish,
|
timestamp_finish: wg.timestampFinish,
|
||||||
withdrawal_group_id: wg.withdrawalGroupId,
|
withdrawal_group_id: wg.withdrawalGroupId,
|
||||||
secret_seed: wg.secretSeed
|
secret_seed: wg.secretSeed,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -425,7 +442,7 @@ export async function exportBackup(
|
|||||||
|
|
||||||
backupPurchases.push({
|
backupPurchases.push({
|
||||||
clock_created: 1,
|
clock_created: 1,
|
||||||
contract_terms_raw: purch.contractTermsRaw,
|
contract_terms_raw: purch.download.contractTermsRaw,
|
||||||
auto_refund_deadline: purch.autoRefundDeadline,
|
auto_refund_deadline: purch.autoRefundDeadline,
|
||||||
merchant_pay_sig: purch.merchantPaySig,
|
merchant_pay_sig: purch.merchantPaySig,
|
||||||
pay_coins: purch.payCoinSelection.coinPubs.map((x, i) => ({
|
pay_coins: purch.payCoinSelection.coinPubs.map((x, i) => ({
|
||||||
@ -478,6 +495,9 @@ export async function exportBackup(
|
|||||||
timestamp: prop.timestamp,
|
timestamp: prop.timestamp,
|
||||||
contract_terms_raw: prop.download?.contractTermsRaw,
|
contract_terms_raw: prop.download?.contractTermsRaw,
|
||||||
download_session_id: prop.downloadSessionId,
|
download_session_id: prop.downloadSessionId,
|
||||||
|
merchant_base_url: prop.merchantBaseUrl,
|
||||||
|
order_id: prop.orderId,
|
||||||
|
merchant_sig: prop.download?.contractData.merchantSig,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -572,9 +592,47 @@ export async function encryptBackup(
|
|||||||
throw Error("not implemented");
|
throw Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CompletedCoin {
|
||||||
|
coinPub: string;
|
||||||
|
coinEvHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Precomputed cryptographic material for a backup import.
|
||||||
|
*
|
||||||
|
* We separate this data from the backup blob as we want the backup
|
||||||
|
* blob to be small, and we can't compute it during the database transaction,
|
||||||
|
* as the async crypto worker communication would auto-close the database transaction.
|
||||||
|
*/
|
||||||
|
interface BackupCryptoPrecomputedData {
|
||||||
|
denomPubToHash: Record<string, string>;
|
||||||
|
coinPrivToCompletedCoin: Record<string, CompletedCoin>;
|
||||||
|
proposalNoncePrivToProposalPub: { [priv: string]: string };
|
||||||
|
proposalIdToContractTermsHash: { [proposalId: string]: string };
|
||||||
|
reservePrivToPub: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkBackupInvariant(b: boolean, m?: string): asserts b {
|
||||||
|
if (!b) {
|
||||||
|
if (m) {
|
||||||
|
throw Error(`BUG: backup invariant failed (${m})`);
|
||||||
|
} else {
|
||||||
|
throw Error("BUG: backup invariant failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDenomSelStateFromBackup(
|
||||||
|
tx: TransactionHandle<typeof Stores.denominations>,
|
||||||
|
sel: BackupDenomSel,
|
||||||
|
): Promise<DenomSelectionState> {
|
||||||
|
throw Error("not implemented");
|
||||||
|
}
|
||||||
|
|
||||||
export async function importBackup(
|
export async function importBackup(
|
||||||
ws: InternalWalletState,
|
ws: InternalWalletState,
|
||||||
backupRequest: BackupRequest,
|
backupRequest: BackupRequest,
|
||||||
|
cryptoComp: BackupCryptoPrecomputedData,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await provideBackupState(ws);
|
await provideBackupState(ws);
|
||||||
return ws.db.runWithWriteTransaction(
|
return ws.db.runWithWriteTransaction(
|
||||||
@ -593,8 +651,439 @@ export async function importBackup(
|
|||||||
Stores.withdrawalGroups,
|
Stores.withdrawalGroups,
|
||||||
],
|
],
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
|
// FIXME: validate schema!
|
||||||
|
const backupBlob = backupRequest.backupBlob as WalletBackupContentV1;
|
||||||
|
|
||||||
});
|
// FIXME: validate version
|
||||||
|
|
||||||
|
for (const backupExchange of backupBlob.exchanges) {
|
||||||
|
const existingExchange = await tx.get(
|
||||||
|
Stores.exchanges,
|
||||||
|
backupExchange.base_url,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!existingExchange) {
|
||||||
|
const wireInfo: ExchangeWireInfo = {
|
||||||
|
accounts: backupExchange.accounts.map((x) => ({
|
||||||
|
master_sig: x.master_sig,
|
||||||
|
payto_uri: x.payto_uri,
|
||||||
|
})),
|
||||||
|
feesForType: {},
|
||||||
|
};
|
||||||
|
for (const fee of backupExchange.wire_fees) {
|
||||||
|
const w = (wireInfo.feesForType[fee.wire_type] ??= []);
|
||||||
|
w.push({
|
||||||
|
closingFee: Amounts.parseOrThrow(fee.closing_fee),
|
||||||
|
endStamp: fee.end_stamp,
|
||||||
|
sig: fee.sig,
|
||||||
|
startStamp: fee.start_stamp,
|
||||||
|
wireFee: Amounts.parseOrThrow(fee.wire_fee),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await tx.put(Stores.exchanges, {
|
||||||
|
addComplete: true,
|
||||||
|
baseUrl: backupExchange.base_url,
|
||||||
|
builtIn: false,
|
||||||
|
updateReason: undefined,
|
||||||
|
permanent: true,
|
||||||
|
retryInfo: initRetryInfo(),
|
||||||
|
termsOfServiceAcceptedEtag: backupExchange.tos_etag_accepted,
|
||||||
|
termsOfServiceText: undefined,
|
||||||
|
termsOfServiceLastEtag: backupExchange.tos_etag_last,
|
||||||
|
updateStarted: getTimestampNow(),
|
||||||
|
updateStatus: ExchangeUpdateStatus.FetchKeys,
|
||||||
|
wireInfo,
|
||||||
|
details: {
|
||||||
|
currency: backupExchange.currency,
|
||||||
|
auditors: backupExchange.auditors.map((x) => ({
|
||||||
|
auditor_pub: x.auditor_pub,
|
||||||
|
auditor_url: x.auditor_url,
|
||||||
|
denomination_keys: x.denomination_keys,
|
||||||
|
})),
|
||||||
|
lastUpdateTime: { t_ms: "never" },
|
||||||
|
masterPublicKey: backupExchange.master_public_key,
|
||||||
|
nextUpdateTime: { t_ms: "never" },
|
||||||
|
protocolVersion: backupExchange.protocol_version,
|
||||||
|
signingKeys: backupExchange.signing_keys.map((x) => ({
|
||||||
|
key: x.key,
|
||||||
|
master_sig: x.master_sig,
|
||||||
|
stamp_end: x.stamp_end,
|
||||||
|
stamp_expire: x.stamp_expire,
|
||||||
|
stamp_start: x.stamp_start,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backupDenomination of backupExchange.denominations) {
|
||||||
|
const denomPubHash =
|
||||||
|
cryptoComp.denomPubToHash[backupDenomination.denom_pub];
|
||||||
|
checkLogicInvariant(!!denomPubHash);
|
||||||
|
const existingDenom = await tx.get(Stores.denominations, [
|
||||||
|
backupExchange.base_url,
|
||||||
|
denomPubHash,
|
||||||
|
]);
|
||||||
|
if (!existingDenom) {
|
||||||
|
await tx.put(Stores.denominations, {
|
||||||
|
denomPub: backupDenomination.denom_pub,
|
||||||
|
denomPubHash: denomPubHash,
|
||||||
|
exchangeBaseUrl: backupExchange.base_url,
|
||||||
|
feeDeposit: Amounts.parseOrThrow(backupDenomination.fee_deposit),
|
||||||
|
feeRefresh: Amounts.parseOrThrow(backupDenomination.fee_refresh),
|
||||||
|
feeRefund: Amounts.parseOrThrow(backupDenomination.fee_refund),
|
||||||
|
feeWithdraw: Amounts.parseOrThrow(
|
||||||
|
backupDenomination.fee_withdraw,
|
||||||
|
),
|
||||||
|
isOffered: backupDenomination.is_offered,
|
||||||
|
isRevoked: backupDenomination.is_revoked,
|
||||||
|
masterSig: backupDenomination.master_sig,
|
||||||
|
stampExpireDeposit: backupDenomination.stamp_expire_deposit,
|
||||||
|
stampExpireLegal: backupDenomination.stamp_expire_legal,
|
||||||
|
stampExpireWithdraw: backupDenomination.stamp_expire_withdraw,
|
||||||
|
stampStart: backupDenomination.stamp_start,
|
||||||
|
status: DenominationStatus.VerifiedGood,
|
||||||
|
value: Amounts.parseOrThrow(backupDenomination.value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const backupCoin of backupDenomination.coins) {
|
||||||
|
const compCoin =
|
||||||
|
cryptoComp.coinPrivToCompletedCoin[backupCoin.coin_priv];
|
||||||
|
checkLogicInvariant(!!compCoin);
|
||||||
|
const existingCoin = await tx.get(Stores.coins, compCoin.coinPub);
|
||||||
|
if (!existingCoin) {
|
||||||
|
let coinSource: CoinSource;
|
||||||
|
switch (backupCoin.coin_source.type) {
|
||||||
|
case BackupCoinSourceType.Refresh:
|
||||||
|
coinSource = {
|
||||||
|
type: CoinSourceType.Refresh,
|
||||||
|
oldCoinPub: backupCoin.coin_source.old_coin_pub,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case BackupCoinSourceType.Tip:
|
||||||
|
coinSource = {
|
||||||
|
type: CoinSourceType.Tip,
|
||||||
|
coinIndex: backupCoin.coin_source.coin_index,
|
||||||
|
walletTipId: backupCoin.coin_source.wallet_tip_id,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case BackupCoinSourceType.Withdraw:
|
||||||
|
coinSource = {
|
||||||
|
type: CoinSourceType.Withdraw,
|
||||||
|
coinIndex: backupCoin.coin_source.coin_index,
|
||||||
|
reservePub: backupCoin.coin_source.reserve_pub,
|
||||||
|
withdrawalGroupId:
|
||||||
|
backupCoin.coin_source.withdrawal_group_id,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await tx.put(Stores.coins, {
|
||||||
|
blindingKey: backupCoin.blinding_key,
|
||||||
|
coinEvHash: compCoin.coinEvHash,
|
||||||
|
coinPriv: backupCoin.coin_priv,
|
||||||
|
currentAmount: Amounts.parseOrThrow(backupCoin.current_amount),
|
||||||
|
denomSig: backupCoin.denom_sig,
|
||||||
|
coinPub: compCoin.coinPub,
|
||||||
|
suspended: false,
|
||||||
|
exchangeBaseUrl: backupExchange.base_url,
|
||||||
|
denomPub: backupDenomination.denom_pub,
|
||||||
|
denomPubHash,
|
||||||
|
status: backupCoin.fresh
|
||||||
|
? CoinStatus.Fresh
|
||||||
|
: CoinStatus.Dormant,
|
||||||
|
coinSource,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backupReserve of backupExchange.reserves) {
|
||||||
|
const reservePub =
|
||||||
|
cryptoComp.reservePrivToPub[backupReserve.reserve_priv];
|
||||||
|
checkLogicInvariant(!!reservePub);
|
||||||
|
const existingReserve = await tx.get(Stores.reserves, reservePub);
|
||||||
|
const instructedAmount = Amounts.parseOrThrow(
|
||||||
|
backupReserve.instructed_amount,
|
||||||
|
);
|
||||||
|
if (!existingReserve) {
|
||||||
|
let bankInfo: ReserveBankInfo | undefined;
|
||||||
|
if (backupReserve.bank_info) {
|
||||||
|
bankInfo = {
|
||||||
|
exchangePaytoUri: backupReserve.bank_info.exchange_payto_uri,
|
||||||
|
statusUrl: backupReserve.bank_info.status_url,
|
||||||
|
confirmUrl: backupReserve.bank_info.confirm_url,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await tx.put(Stores.reserves, {
|
||||||
|
currency: instructedAmount.currency,
|
||||||
|
instructedAmount,
|
||||||
|
exchangeBaseUrl: backupExchange.base_url,
|
||||||
|
reservePub,
|
||||||
|
reservePriv: backupReserve.reserve_priv,
|
||||||
|
requestedQuery: false,
|
||||||
|
bankInfo,
|
||||||
|
timestampCreated: backupReserve.timestamp_created,
|
||||||
|
timestampBankConfirmed:
|
||||||
|
backupReserve.bank_info?.timestamp_bank_confirmed,
|
||||||
|
timestampReserveInfoPosted:
|
||||||
|
backupReserve.bank_info?.timestamp_reserve_info_posted,
|
||||||
|
senderWire: backupReserve.sender_wire,
|
||||||
|
retryInfo: initRetryInfo(false),
|
||||||
|
lastError: undefined,
|
||||||
|
lastSuccessfulStatusQuery: { t_ms: "never" },
|
||||||
|
initialWithdrawalGroupId:
|
||||||
|
backupReserve.initial_withdrawal_group_id,
|
||||||
|
initialWithdrawalStarted:
|
||||||
|
backupReserve.withdrawal_groups.length > 0,
|
||||||
|
// FIXME!
|
||||||
|
reserveStatus: ReserveRecordStatus.QUERYING_STATUS,
|
||||||
|
initialDenomSel: await getDenomSelStateFromBackup(
|
||||||
|
tx,
|
||||||
|
backupReserve.initial_selected_denoms,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (const backupWg of backupReserve.withdrawal_groups) {
|
||||||
|
const existingWg = await tx.get(
|
||||||
|
Stores.withdrawalGroups,
|
||||||
|
backupWg.withdrawal_group_id,
|
||||||
|
);
|
||||||
|
if (!existingWg) {
|
||||||
|
await tx.put(Stores.withdrawalGroups, {
|
||||||
|
denomsSel: await getDenomSelStateFromBackup(
|
||||||
|
tx,
|
||||||
|
backupWg.selected_denoms,
|
||||||
|
),
|
||||||
|
exchangeBaseUrl: backupExchange.base_url,
|
||||||
|
lastError: undefined,
|
||||||
|
rawWithdrawalAmount: Amounts.parseOrThrow(
|
||||||
|
backupWg.raw_withdrawal_amount,
|
||||||
|
),
|
||||||
|
reservePub,
|
||||||
|
retryInfo: initRetryInfo(false),
|
||||||
|
secretSeed: backupWg.secret_seed,
|
||||||
|
timestampStart: backupWg.timestamp_start,
|
||||||
|
timestampFinish: backupWg.timestamp_finish,
|
||||||
|
withdrawalGroupId: backupWg.withdrawal_group_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backupProposal of backupBlob.proposals) {
|
||||||
|
const existingProposal = await tx.get(
|
||||||
|
Stores.proposals,
|
||||||
|
backupProposal.proposal_id,
|
||||||
|
);
|
||||||
|
if (!existingProposal) {
|
||||||
|
let download: ProposalDownload | undefined;
|
||||||
|
let proposalStatus: ProposalStatus;
|
||||||
|
switch (backupProposal.proposal_status) {
|
||||||
|
case BackupProposalStatus.Proposed:
|
||||||
|
if (backupProposal.contract_terms_raw) {
|
||||||
|
proposalStatus = ProposalStatus.PROPOSED;
|
||||||
|
} else {
|
||||||
|
proposalStatus = ProposalStatus.DOWNLOADING;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BackupProposalStatus.Refused:
|
||||||
|
proposalStatus = ProposalStatus.REFUSED;
|
||||||
|
break;
|
||||||
|
case BackupProposalStatus.Repurchase:
|
||||||
|
proposalStatus = ProposalStatus.REPURCHASE;
|
||||||
|
break;
|
||||||
|
case BackupProposalStatus.PermanentlyFailed:
|
||||||
|
proposalStatus = ProposalStatus.PERMANENTLY_FAILED;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (backupProposal.contract_terms_raw) {
|
||||||
|
checkDbInvariant(!!backupProposal.merchant_sig);
|
||||||
|
const parsedContractTerms = codecForContractTerms().decode(
|
||||||
|
backupProposal.contract_terms_raw,
|
||||||
|
);
|
||||||
|
const amount = Amounts.parseOrThrow(parsedContractTerms.amount);
|
||||||
|
const contractTermsHash =
|
||||||
|
cryptoComp.proposalIdToContractTermsHash[
|
||||||
|
backupProposal.proposal_id
|
||||||
|
];
|
||||||
|
let maxWireFee: AmountJson;
|
||||||
|
if (parsedContractTerms.max_wire_fee) {
|
||||||
|
maxWireFee = Amounts.parseOrThrow(
|
||||||
|
parsedContractTerms.max_wire_fee,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
maxWireFee = Amounts.getZero(amount.currency);
|
||||||
|
}
|
||||||
|
download = {
|
||||||
|
contractData: {
|
||||||
|
amount,
|
||||||
|
contractTermsHash: contractTermsHash,
|
||||||
|
fulfillmentUrl: parsedContractTerms.fulfillment_url ?? "",
|
||||||
|
merchantBaseUrl: parsedContractTerms.merchant_base_url,
|
||||||
|
merchantPub: parsedContractTerms.merchant_pub,
|
||||||
|
merchantSig: backupProposal.merchant_sig,
|
||||||
|
orderId: parsedContractTerms.order_id,
|
||||||
|
summary: parsedContractTerms.summary,
|
||||||
|
autoRefund: parsedContractTerms.auto_refund,
|
||||||
|
maxWireFee,
|
||||||
|
payDeadline: parsedContractTerms.pay_deadline,
|
||||||
|
refundDeadline: parsedContractTerms.refund_deadline,
|
||||||
|
wireFeeAmortization:
|
||||||
|
parsedContractTerms.wire_fee_amortization || 1,
|
||||||
|
allowedAuditors: parsedContractTerms.auditors.map((x) => ({
|
||||||
|
auditorBaseUrl: x.url,
|
||||||
|
auditorPub: x.master_pub,
|
||||||
|
})),
|
||||||
|
allowedExchanges: parsedContractTerms.exchanges.map((x) => ({
|
||||||
|
exchangeBaseUrl: x.url,
|
||||||
|
exchangePub: x.master_pub,
|
||||||
|
})),
|
||||||
|
timestamp: parsedContractTerms.timestamp,
|
||||||
|
wireMethod: parsedContractTerms.wire_method,
|
||||||
|
wireInfoHash: parsedContractTerms.h_wire,
|
||||||
|
maxDepositFee: Amounts.parseOrThrow(
|
||||||
|
parsedContractTerms.max_fee,
|
||||||
|
),
|
||||||
|
merchant: parsedContractTerms.merchant,
|
||||||
|
products: parsedContractTerms.products,
|
||||||
|
summaryI18n: parsedContractTerms.summary_i18n,
|
||||||
|
},
|
||||||
|
contractTermsRaw: backupProposal.contract_terms_raw,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
await tx.put(Stores.proposals, {
|
||||||
|
claimToken: backupProposal.claim_token,
|
||||||
|
lastError: undefined,
|
||||||
|
merchantBaseUrl: backupProposal.merchant_base_url,
|
||||||
|
timestamp: backupProposal.timestamp,
|
||||||
|
orderId: backupProposal.order_id,
|
||||||
|
noncePriv: backupProposal.nonce_priv,
|
||||||
|
noncePub:
|
||||||
|
cryptoComp.proposalNoncePrivToProposalPub[
|
||||||
|
backupProposal.nonce_priv
|
||||||
|
],
|
||||||
|
proposalId: backupProposal.proposal_id,
|
||||||
|
repurchaseProposalId: backupProposal.repurchase_proposal_id,
|
||||||
|
retryInfo: initRetryInfo(false),
|
||||||
|
download,
|
||||||
|
proposalStatus,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backupPurchase of backupBlob.purchases) {
|
||||||
|
const existingPurchase = await tx.get(
|
||||||
|
Stores.purchases,
|
||||||
|
backupPurchase.proposal_id,
|
||||||
|
);
|
||||||
|
if (!existingPurchase) {
|
||||||
|
await tx.put(Stores.purchases, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backupRefreshGroup of backupBlob.refresh_groups) {
|
||||||
|
const existingRg = await tx.get(
|
||||||
|
Stores.refreshGroups,
|
||||||
|
backupRefreshGroup.refresh_group_id,
|
||||||
|
);
|
||||||
|
if (!existingRg) {
|
||||||
|
let reason: RefreshReason;
|
||||||
|
switch (backupRefreshGroup.reason) {
|
||||||
|
case BackupRefreshReason.AbortPay:
|
||||||
|
reason = RefreshReason.AbortPay;
|
||||||
|
break;
|
||||||
|
case BackupRefreshReason.BackupRestored:
|
||||||
|
reason = RefreshReason.BackupRestored;
|
||||||
|
break;
|
||||||
|
case BackupRefreshReason.Manual:
|
||||||
|
reason = RefreshReason.Manual;
|
||||||
|
break;
|
||||||
|
case BackupRefreshReason.Pay:
|
||||||
|
reason = RefreshReason.Pay;
|
||||||
|
break;
|
||||||
|
case BackupRefreshReason.Recoup:
|
||||||
|
reason = RefreshReason.Recoup;
|
||||||
|
break;
|
||||||
|
case BackupRefreshReason.Refund:
|
||||||
|
reason = RefreshReason.Refund;
|
||||||
|
break;
|
||||||
|
case BackupRefreshReason.Scheduled:
|
||||||
|
reason = RefreshReason.Scheduled;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const refreshSessionPerCoin: (
|
||||||
|
| RefreshSessionRecord
|
||||||
|
| undefined
|
||||||
|
)[] = [];
|
||||||
|
for (const oldCoin of backupRefreshGroup.old_coins) {
|
||||||
|
if (oldCoin.refresh_session) {
|
||||||
|
const denomSel = await getDenomSelStateFromBackup(
|
||||||
|
tx,
|
||||||
|
oldCoin.refresh_session.new_denoms,
|
||||||
|
);
|
||||||
|
refreshSessionPerCoin.push({
|
||||||
|
sessionSecretSeed: oldCoin.refresh_session.session_secret_seed,
|
||||||
|
norevealIndex: oldCoin.refresh_session.noreveal_index,
|
||||||
|
newDenoms: oldCoin.refresh_session.new_denoms.map((x) => ({
|
||||||
|
count: x.count,
|
||||||
|
denomPubHash: x.denom_pub_hash,
|
||||||
|
})),
|
||||||
|
amountRefreshOutput: denomSel.totalCoinValue,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
refreshSessionPerCoin.push(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await tx.put(Stores.refreshGroups, {
|
||||||
|
timestampFinished: backupRefreshGroup.timestamp_finished,
|
||||||
|
timestampCreated: backupRefreshGroup.timestamp_started,
|
||||||
|
refreshGroupId: backupRefreshGroup.refresh_group_id,
|
||||||
|
reason,
|
||||||
|
lastError: undefined,
|
||||||
|
lastErrorPerCoin: {},
|
||||||
|
oldCoinPubs: backupRefreshGroup.old_coins.map((x) => x.coin_pub),
|
||||||
|
finishedPerCoin: backupRefreshGroup.old_coins.map(
|
||||||
|
(x) => x.finished,
|
||||||
|
),
|
||||||
|
inputPerCoin: backupRefreshGroup.old_coins.map((x) =>
|
||||||
|
Amounts.parseOrThrow(x.input_amount),
|
||||||
|
),
|
||||||
|
estimatedOutputPerCoin: backupRefreshGroup.old_coins.map((x) =>
|
||||||
|
Amounts.parseOrThrow(x.estimated_output_amount),
|
||||||
|
),
|
||||||
|
refreshSessionPerCoin,
|
||||||
|
retryInfo: initRetryInfo(false),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const backupTip of backupBlob.tips) {
|
||||||
|
const existingTip = await tx.get(Stores.tips, backupTip.wallet_tip_id);
|
||||||
|
if (!existingTip) {
|
||||||
|
const denomsSel = await getDenomSelStateFromBackup(
|
||||||
|
tx,
|
||||||
|
backupTip.selected_denoms,
|
||||||
|
);
|
||||||
|
await tx.put(Stores.tips, {
|
||||||
|
acceptedTimestamp: backupTip.timestamp_accepted,
|
||||||
|
createdTimestamp: backupTip.timestamp_created,
|
||||||
|
denomsSel,
|
||||||
|
exchangeBaseUrl: backupTip.exchange_base_url,
|
||||||
|
lastError: undefined,
|
||||||
|
merchantBaseUrl: backupTip.exchange_base_url,
|
||||||
|
merchantTipId: backupTip.merchant_tip_id,
|
||||||
|
pickedUpTimestamp: backupTip.timestam_picked_up,
|
||||||
|
retryInfo: initRetryInfo(false),
|
||||||
|
secretSeed: backupTip.secret_seed,
|
||||||
|
tipAmountEffective: denomsSel.totalCoinValue,
|
||||||
|
tipAmountRaw: Amounts.parseOrThrow(backupTip.tip_amount_raw),
|
||||||
|
tipExpiration: backupTip.timestamp_expiration,
|
||||||
|
walletTipId: backupTip.wallet_tip_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function deriveAccountKeyPair(
|
function deriveAccountKeyPair(
|
||||||
@ -607,7 +1096,6 @@ function deriveAccountKeyPair(
|
|||||||
stringToBytes("taler-sync-account-key-salt"),
|
stringToBytes("taler-sync-account-key-salt"),
|
||||||
stringToBytes(providerUrl),
|
stringToBytes(providerUrl),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
eddsaPriv: privateKey,
|
eddsaPriv: privateKey,
|
||||||
eddsaPub: eddsaGetPublic(privateKey),
|
eddsaPub: eddsaGetPublic(privateKey),
|
||||||
|
@ -441,8 +441,7 @@ async function recordConfirmPay(
|
|||||||
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
|
const payCostInfo = await getTotalPaymentCost(ws, coinSelection);
|
||||||
const t: PurchaseRecord = {
|
const t: PurchaseRecord = {
|
||||||
abortStatus: AbortStatus.None,
|
abortStatus: AbortStatus.None,
|
||||||
contractTermsRaw: d.contractTermsRaw,
|
download: d,
|
||||||
contractData: d.contractData,
|
|
||||||
lastSessionId: sessionId,
|
lastSessionId: sessionId,
|
||||||
payCoinSelection: coinSelection,
|
payCoinSelection: coinSelection,
|
||||||
totalPayCost: payCostInfo,
|
totalPayCost: payCostInfo,
|
||||||
@ -763,7 +762,7 @@ async function processDownloadProposalImpl(
|
|||||||
products: parsedContractTerms.products,
|
products: parsedContractTerms.products,
|
||||||
summaryI18n: parsedContractTerms.summary_i18n,
|
summaryI18n: parsedContractTerms.summary_i18n,
|
||||||
},
|
},
|
||||||
contractTermsRaw: JSON.stringify(proposalResp.contract_terms),
|
contractTermsRaw: proposalResp.contract_terms,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
fulfillmentUrl &&
|
fulfillmentUrl &&
|
||||||
@ -877,7 +876,7 @@ async function storeFirstPaySuccess(
|
|||||||
purchase.payRetryInfo = initRetryInfo(false);
|
purchase.payRetryInfo = initRetryInfo(false);
|
||||||
purchase.merchantPaySig = paySig;
|
purchase.merchantPaySig = paySig;
|
||||||
if (isFirst) {
|
if (isFirst) {
|
||||||
const ar = purchase.contractData.autoRefund;
|
const ar = purchase.download.contractData.autoRefund;
|
||||||
if (ar) {
|
if (ar) {
|
||||||
logger.info("auto_refund present");
|
logger.info("auto_refund present");
|
||||||
purchase.refundQueryRequested = true;
|
purchase.refundQueryRequested = true;
|
||||||
@ -938,8 +937,8 @@ async function submitPay(
|
|||||||
|
|
||||||
if (!purchase.merchantPaySig) {
|
if (!purchase.merchantPaySig) {
|
||||||
const payUrl = new URL(
|
const payUrl = new URL(
|
||||||
`orders/${purchase.contractData.orderId}/pay`,
|
`orders/${purchase.download.contractData.orderId}/pay`,
|
||||||
purchase.contractData.merchantBaseUrl,
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
).href;
|
).href;
|
||||||
|
|
||||||
const reqBody = {
|
const reqBody = {
|
||||||
@ -986,10 +985,10 @@ async function submitPay(
|
|||||||
|
|
||||||
logger.trace("got success from pay URL", merchantResp);
|
logger.trace("got success from pay URL", merchantResp);
|
||||||
|
|
||||||
const merchantPub = purchase.contractData.merchantPub;
|
const merchantPub = purchase.download.contractData.merchantPub;
|
||||||
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
const valid: boolean = await ws.cryptoApi.isValidPaymentSignature(
|
||||||
merchantResp.sig,
|
merchantResp.sig,
|
||||||
purchase.contractData.contractTermsHash,
|
purchase.download.contractData.contractTermsHash,
|
||||||
merchantPub,
|
merchantPub,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -1002,12 +1001,12 @@ async function submitPay(
|
|||||||
await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
|
await storeFirstPaySuccess(ws, proposalId, sessionId, merchantResp.sig);
|
||||||
} else {
|
} else {
|
||||||
const payAgainUrl = new URL(
|
const payAgainUrl = new URL(
|
||||||
`orders/${purchase.contractData.orderId}/paid`,
|
`orders/${purchase.download.contractData.orderId}/paid`,
|
||||||
purchase.contractData.merchantBaseUrl,
|
purchase.download.contractData.merchantBaseUrl,
|
||||||
).href;
|
).href;
|
||||||
const reqBody = {
|
const reqBody = {
|
||||||
sig: purchase.merchantPaySig,
|
sig: purchase.merchantPaySig,
|
||||||
h_contract: purchase.contractData.contractTermsHash,
|
h_contract: purchase.download.contractData.contractTermsHash,
|
||||||
session_id: sessionId ?? "",
|
session_id: sessionId ?? "",
|
||||||
};
|
};
|
||||||
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
|
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], () =>
|
||||||
@ -1047,7 +1046,7 @@ async function submitPay(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
type: ConfirmPayResultType.Done,
|
type: ConfirmPayResultType.Done,
|
||||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
contractTerms: purchase.download.contractTermsRaw,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1120,7 +1119,7 @@ export async function preparePayForUri(
|
|||||||
logger.info("not confirming payment, insufficient coins");
|
logger.info("not confirming payment, insufficient coins");
|
||||||
return {
|
return {
|
||||||
status: PreparePayResultType.InsufficientBalance,
|
status: PreparePayResultType.InsufficientBalance,
|
||||||
contractTerms: JSON.parse(d.contractTermsRaw),
|
contractTerms: d.contractTermsRaw,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
amountRaw: Amounts.stringify(d.contractData.amount),
|
amountRaw: Amounts.stringify(d.contractData.amount),
|
||||||
};
|
};
|
||||||
@ -1132,7 +1131,7 @@ export async function preparePayForUri(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
status: PreparePayResultType.PaymentPossible,
|
status: PreparePayResultType.PaymentPossible,
|
||||||
contractTerms: JSON.parse(d.contractTermsRaw),
|
contractTerms: d.contractTermsRaw,
|
||||||
proposalId: proposal.proposalId,
|
proposalId: proposal.proposalId,
|
||||||
amountEffective: Amounts.stringify(totalCost),
|
amountEffective: Amounts.stringify(totalCost),
|
||||||
amountRaw: Amounts.stringify(res.paymentAmount),
|
amountRaw: Amounts.stringify(res.paymentAmount),
|
||||||
@ -1161,20 +1160,20 @@ export async function preparePayForUri(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
contractTerms: purchase.download.contractTermsRaw,
|
||||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
paid: true,
|
paid: true,
|
||||||
amountRaw: Amounts.stringify(purchase.contractData.amount),
|
amountRaw: Amounts.stringify(purchase.download.contractData.amount),
|
||||||
amountEffective: Amounts.stringify(purchase.totalPayCost),
|
amountEffective: Amounts.stringify(purchase.totalPayCost),
|
||||||
proposalId,
|
proposalId,
|
||||||
};
|
};
|
||||||
} else if (!purchase.timestampFirstSuccessfulPay) {
|
} else if (!purchase.timestampFirstSuccessfulPay) {
|
||||||
return {
|
return {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
contractTerms: purchase.download.contractTermsRaw,
|
||||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
paid: false,
|
paid: false,
|
||||||
amountRaw: Amounts.stringify(purchase.contractData.amount),
|
amountRaw: Amounts.stringify(purchase.download.contractData.amount),
|
||||||
amountEffective: Amounts.stringify(purchase.totalPayCost),
|
amountEffective: Amounts.stringify(purchase.totalPayCost),
|
||||||
proposalId,
|
proposalId,
|
||||||
};
|
};
|
||||||
@ -1182,12 +1181,12 @@ export async function preparePayForUri(
|
|||||||
const paid = !purchase.paymentSubmitPending;
|
const paid = !purchase.paymentSubmitPending;
|
||||||
return {
|
return {
|
||||||
status: PreparePayResultType.AlreadyConfirmed,
|
status: PreparePayResultType.AlreadyConfirmed,
|
||||||
contractTerms: JSON.parse(purchase.contractTermsRaw),
|
contractTerms: purchase.download.contractTermsRaw,
|
||||||
contractTermsHash: purchase.contractData.contractTermsHash,
|
contractTermsHash: purchase.download.contractData.contractTermsHash,
|
||||||
paid,
|
paid,
|
||||||
amountRaw: Amounts.stringify(purchase.contractData.amount),
|
amountRaw: Amounts.stringify(purchase.download.contractData.amount),
|
||||||
amountEffective: Amounts.stringify(purchase.totalPayCost),
|
amountEffective: Amounts.stringify(purchase.totalPayCost),
|
||||||
...(paid ? { nextUrl: purchase.contractData.orderId } : {}),
|
...(paid ? { nextUrl: purchase.download.contractData.orderId } : {}),
|
||||||
proposalId,
|
proposalId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -33,11 +33,15 @@
|
|||||||
* aren't exported yet (and not even implemented in wallet-core).
|
* aren't exported yet (and not even implemented in wallet-core).
|
||||||
* 6. Returning money to own bank account isn't supported/exported yet.
|
* 6. Returning money to own bank account isn't supported/exported yet.
|
||||||
* 7. Peer-to-peer payments aren't supported yet.
|
* 7. Peer-to-peer payments aren't supported yet.
|
||||||
|
* 8. Next update time / next refresh time isn't backed up yet.
|
||||||
*
|
*
|
||||||
* Questions:
|
* Questions:
|
||||||
* 1. What happens when two backups are merged that have
|
* 1. What happens when two backups are merged that have
|
||||||
* the same coin in different refresh groups?
|
* the same coin in different refresh groups?
|
||||||
* => Both are added, one will eventually fail
|
* => Both are added, one will eventually fail
|
||||||
|
* 2. Should we make more information forgettable? I.e. is
|
||||||
|
* the coin selection still relevant for a purchase after the coins
|
||||||
|
* are legally expired?
|
||||||
*
|
*
|
||||||
* General considerations / decisions:
|
* General considerations / decisions:
|
||||||
* 1. Information about previously occurring errors and
|
* 1. Information about previously occurring errors and
|
||||||
@ -74,6 +78,8 @@ type DeviceIdString = string;
|
|||||||
*/
|
*/
|
||||||
type ClockValue = number;
|
type ClockValue = number;
|
||||||
|
|
||||||
|
type RawContractTerms = any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Content of the backup.
|
* Content of the backup.
|
||||||
*
|
*
|
||||||
@ -544,10 +550,7 @@ export interface BackupRefreshSession {
|
|||||||
/**
|
/**
|
||||||
* Hased denominations of the newly requested coins.
|
* Hased denominations of the newly requested coins.
|
||||||
*/
|
*/
|
||||||
new_denoms: {
|
new_denoms: BackupDenomSel;
|
||||||
count: number;
|
|
||||||
denom_pub_hash: string;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Seed used to derive the planchets and
|
* Seed used to derive the planchets and
|
||||||
@ -654,10 +657,7 @@ export interface BackupWithdrawalGroup {
|
|||||||
/**
|
/**
|
||||||
* Multiset of denominations selected for withdrawal.
|
* Multiset of denominations selected for withdrawal.
|
||||||
*/
|
*/
|
||||||
selected_denoms: {
|
selected_denoms: BackupDenomSel;
|
||||||
denom_pub_hash: string;
|
|
||||||
count: number;
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum BackupRefundState {
|
export enum BackupRefundState {
|
||||||
@ -747,7 +747,14 @@ export interface BackupPurchase {
|
|||||||
/**
|
/**
|
||||||
* Contract terms we got from the merchant.
|
* Contract terms we got from the merchant.
|
||||||
*/
|
*/
|
||||||
contract_terms_raw: string;
|
contract_terms_raw: RawContractTerms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature on the contract terms.
|
||||||
|
*
|
||||||
|
* Must be present if contract_terms_raw is present.
|
||||||
|
*/
|
||||||
|
merchant_sig?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Private key for the nonce. Might eventually be used
|
* Private key for the nonce. Might eventually be used
|
||||||
@ -889,6 +896,14 @@ export interface BackupDenomination {
|
|||||||
coins: BackupCoin[];
|
coins: BackupCoin[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Denomination selection.
|
||||||
|
*/
|
||||||
|
export type BackupDenomSel = {
|
||||||
|
denom_pub_hash: string;
|
||||||
|
count: number;
|
||||||
|
}[];
|
||||||
|
|
||||||
export interface BackupReserve {
|
export interface BackupReserve {
|
||||||
/**
|
/**
|
||||||
* The reserve private key.
|
* The reserve private key.
|
||||||
@ -961,10 +976,7 @@ export interface BackupReserve {
|
|||||||
* Denominations selected for the initial withdrawal.
|
* Denominations selected for the initial withdrawal.
|
||||||
* Stored here to show costs before withdrawal has begun.
|
* Stored here to show costs before withdrawal has begun.
|
||||||
*/
|
*/
|
||||||
initial_selected_denoms: {
|
initial_selected_denoms: BackupDenomSel;
|
||||||
denom_pub_hash: string;
|
|
||||||
count: number;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Groups of withdrawal operations for this reserve. Typically just one.
|
* Groups of withdrawal operations for this reserve. Typically just one.
|
||||||
@ -1126,10 +1138,6 @@ export enum BackupProposalStatus {
|
|||||||
* but the user needs to accept/reject it.
|
* but the user needs to accept/reject it.
|
||||||
*/
|
*/
|
||||||
Proposed = "proposed",
|
Proposed = "proposed",
|
||||||
/**
|
|
||||||
* The user has accepted the proposal.
|
|
||||||
*/
|
|
||||||
Accepted = "accepted",
|
|
||||||
/**
|
/**
|
||||||
* The user has rejected the proposal.
|
* The user has rejected the proposal.
|
||||||
*/
|
*/
|
||||||
@ -1150,16 +1158,33 @@ export enum BackupProposalStatus {
|
|||||||
* Proposal by a merchant.
|
* Proposal by a merchant.
|
||||||
*/
|
*/
|
||||||
export interface BackupProposal {
|
export interface BackupProposal {
|
||||||
|
/**
|
||||||
|
* Base URL of the merchant that proposed the purchase.
|
||||||
|
*/
|
||||||
|
merchant_base_url: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Downloaded data from the merchant.
|
* Downloaded data from the merchant.
|
||||||
*/
|
*/
|
||||||
contract_terms_raw?: string;
|
contract_terms_raw?: RawContractTerms;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signature on the contract terms.
|
||||||
|
*
|
||||||
|
* Must be present if contract_terms_raw is present.
|
||||||
|
*/
|
||||||
|
merchant_sig?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unique ID when the order is stored in the wallet DB.
|
* Unique ID when the order is stored in the wallet DB.
|
||||||
*/
|
*/
|
||||||
proposal_id: string;
|
proposal_id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merchant-assigned order ID of the proposal.
|
||||||
|
*/
|
||||||
|
order_id: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Timestamp of when the record
|
* Timestamp of when the record
|
||||||
* was created.
|
* was created.
|
||||||
|
@ -753,7 +753,7 @@ export interface ProposalDownload {
|
|||||||
/**
|
/**
|
||||||
* The contract that was offered by the merchant.
|
* The contract that was offered by the merchant.
|
||||||
*/
|
*/
|
||||||
contractTermsRaw: string;
|
contractTermsRaw: any;
|
||||||
|
|
||||||
contractData: WalletContractData;
|
contractData: WalletContractData;
|
||||||
}
|
}
|
||||||
@ -1200,14 +1200,9 @@ export interface PurchaseRecord {
|
|||||||
noncePub: string;
|
noncePub: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Contract terms we got from the merchant.
|
* Downloaded and parsed proposal data.
|
||||||
*/
|
*/
|
||||||
contractTermsRaw: string;
|
download: ProposalDownload;
|
||||||
|
|
||||||
/**
|
|
||||||
* Parsed contract terms.
|
|
||||||
*/
|
|
||||||
contractData: WalletContractData;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deposit permissions, available once the user has accepted the payment.
|
* Deposit permissions, available once the user has accepted the payment.
|
||||||
@ -1291,6 +1286,9 @@ export interface ConfigRecord<T> {
|
|||||||
value: T;
|
value: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* FIXME: Eliminate this in favor of DenomSelectionState.
|
||||||
|
*/
|
||||||
export interface DenominationSelectionInfo {
|
export interface DenominationSelectionInfo {
|
||||||
totalCoinValue: AmountJson;
|
totalCoinValue: AmountJson;
|
||||||
totalWithdrawCost: AmountJson;
|
totalWithdrawCost: AmountJson;
|
||||||
@ -1303,6 +1301,9 @@ export interface DenominationSelectionInfo {
|
|||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected denominations withn some extra info.
|
||||||
|
*/
|
||||||
export interface DenomSelectionState {
|
export interface DenomSelectionState {
|
||||||
totalCoinValue: AmountJson;
|
totalCoinValue: AmountJson;
|
||||||
totalWithdrawCost: AmountJson;
|
totalWithdrawCost: AmountJson;
|
||||||
|
276
packages/taler-wallet-core/src/types/pendingTypes.ts
Normal file
276
packages/taler-wallet-core/src/types/pendingTypes.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/*
|
||||||
|
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/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type and schema definitions for pending operations in the wallet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import { TalerErrorDetails, BalancesResponse } from "./walletTypes";
|
||||||
|
import { ReserveRecordStatus } from "./dbTypes";
|
||||||
|
import { Timestamp, Duration } from "../util/time";
|
||||||
|
import { RetryInfo } from "../util/retries";
|
||||||
|
|
||||||
|
export enum PendingOperationType {
|
||||||
|
Bug = "bug",
|
||||||
|
ExchangeUpdate = "exchange-update",
|
||||||
|
ExchangeCheckRefresh = "exchange-check-refresh",
|
||||||
|
Pay = "pay",
|
||||||
|
ProposalChoice = "proposal-choice",
|
||||||
|
ProposalDownload = "proposal-download",
|
||||||
|
Refresh = "refresh",
|
||||||
|
Reserve = "reserve",
|
||||||
|
Recoup = "recoup",
|
||||||
|
RefundQuery = "refund-query",
|
||||||
|
TipChoice = "tip-choice",
|
||||||
|
TipPickup = "tip-pickup",
|
||||||
|
Withdraw = "withdraw",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Information about a pending operation.
|
||||||
|
*/
|
||||||
|
export type PendingOperationInfo = PendingOperationInfoCommon &
|
||||||
|
(
|
||||||
|
| PendingBugOperation
|
||||||
|
| PendingExchangeUpdateOperation
|
||||||
|
| PendingExchangeCheckRefreshOperation
|
||||||
|
| PendingPayOperation
|
||||||
|
| PendingProposalChoiceOperation
|
||||||
|
| PendingProposalDownloadOperation
|
||||||
|
| PendingRefreshOperation
|
||||||
|
| PendingRefundQueryOperation
|
||||||
|
| PendingReserveOperation
|
||||||
|
| PendingTipChoiceOperation
|
||||||
|
| PendingTipPickupOperation
|
||||||
|
| PendingWithdrawOperation
|
||||||
|
| PendingRecoupOperation
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wallet is currently updating information about an exchange.
|
||||||
|
*/
|
||||||
|
export interface PendingExchangeUpdateOperation {
|
||||||
|
type: PendingOperationType.ExchangeUpdate;
|
||||||
|
stage: ExchangeUpdateOperationStage;
|
||||||
|
reason: string;
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
lastError: TalerErrorDetails | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wallet should check whether coins from this exchange
|
||||||
|
* need to be auto-refreshed.
|
||||||
|
*/
|
||||||
|
export interface PendingExchangeCheckRefreshOperation {
|
||||||
|
type: PendingOperationType.ExchangeCheckRefresh;
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some interal error happened in the wallet. This pending operation
|
||||||
|
* should *only* be reported for problems in the wallet, not when
|
||||||
|
* a problem with a merchant/exchange/etc. occurs.
|
||||||
|
*/
|
||||||
|
export interface PendingBugOperation {
|
||||||
|
type: PendingOperationType.Bug;
|
||||||
|
message: string;
|
||||||
|
details: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current state of an exchange update operation.
|
||||||
|
*/
|
||||||
|
export enum ExchangeUpdateOperationStage {
|
||||||
|
FetchKeys = "fetch-keys",
|
||||||
|
FetchWire = "fetch-wire",
|
||||||
|
FinalizeUpdate = "finalize-update",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ReserveType {
|
||||||
|
/**
|
||||||
|
* Manually created.
|
||||||
|
*/
|
||||||
|
Manual = "manual",
|
||||||
|
/**
|
||||||
|
* Withdrawn from a bank that has "tight" Taler integration
|
||||||
|
*/
|
||||||
|
TalerBankWithdraw = "taler-bank-withdraw",
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of processing a reserve.
|
||||||
|
*
|
||||||
|
* Does *not* include the withdrawal operation that might result
|
||||||
|
* from this.
|
||||||
|
*/
|
||||||
|
export interface PendingReserveOperation {
|
||||||
|
type: PendingOperationType.Reserve;
|
||||||
|
retryInfo: RetryInfo | undefined;
|
||||||
|
stage: ReserveRecordStatus;
|
||||||
|
timestampCreated: Timestamp;
|
||||||
|
reserveType: ReserveType;
|
||||||
|
reservePub: string;
|
||||||
|
bankWithdrawConfirmUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of an ongoing withdrawal operation.
|
||||||
|
*/
|
||||||
|
export interface PendingRefreshOperation {
|
||||||
|
type: PendingOperationType.Refresh;
|
||||||
|
lastError?: TalerErrorDetails;
|
||||||
|
refreshGroupId: string;
|
||||||
|
finishedPerCoin: boolean[];
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of downloading signed contract terms from a merchant.
|
||||||
|
*/
|
||||||
|
export interface PendingProposalDownloadOperation {
|
||||||
|
type: PendingOperationType.ProposalDownload;
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
proposalTimestamp: Timestamp;
|
||||||
|
proposalId: string;
|
||||||
|
orderId: string;
|
||||||
|
lastError?: TalerErrorDetails;
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User must choose whether to accept or reject the merchant's
|
||||||
|
* proposed contract terms.
|
||||||
|
*/
|
||||||
|
export interface PendingProposalChoiceOperation {
|
||||||
|
type: PendingOperationType.ProposalChoice;
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
proposalTimestamp: Timestamp;
|
||||||
|
proposalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wallet is picking up a tip that the user has accepted.
|
||||||
|
*/
|
||||||
|
export interface PendingTipPickupOperation {
|
||||||
|
type: PendingOperationType.TipPickup;
|
||||||
|
tipId: string;
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
merchantTipId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wallet has been offered a tip, and the user now needs to
|
||||||
|
* decide whether to accept or reject the tip.
|
||||||
|
*/
|
||||||
|
export interface PendingTipChoiceOperation {
|
||||||
|
type: PendingOperationType.TipChoice;
|
||||||
|
tipId: string;
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
merchantTipId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wallet is signing coins and then sending them to
|
||||||
|
* the merchant.
|
||||||
|
*/
|
||||||
|
export interface PendingPayOperation {
|
||||||
|
type: PendingOperationType.Pay;
|
||||||
|
proposalId: string;
|
||||||
|
isReplay: boolean;
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
lastError: TalerErrorDetails | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The wallet is querying the merchant about whether any refund
|
||||||
|
* permissions are available for a purchase.
|
||||||
|
*/
|
||||||
|
export interface PendingRefundQueryOperation {
|
||||||
|
type: PendingOperationType.RefundQuery;
|
||||||
|
proposalId: string;
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
lastError: TalerErrorDetails | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PendingRecoupOperation {
|
||||||
|
type: PendingOperationType.Recoup;
|
||||||
|
recoupGroupId: string;
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
lastError: TalerErrorDetails | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Status of an ongoing withdrawal operation.
|
||||||
|
*/
|
||||||
|
export interface PendingWithdrawOperation {
|
||||||
|
type: PendingOperationType.Withdraw;
|
||||||
|
lastError: TalerErrorDetails | undefined;
|
||||||
|
retryInfo: RetryInfo;
|
||||||
|
withdrawalGroupId: string;
|
||||||
|
numCoinsWithdrawn: number;
|
||||||
|
numCoinsTotal: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields that are present in every pending operation.
|
||||||
|
*/
|
||||||
|
export interface PendingOperationInfoCommon {
|
||||||
|
/**
|
||||||
|
* Type of the pending operation.
|
||||||
|
*/
|
||||||
|
type: PendingOperationType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to true if the operation indicates that something is really in progress,
|
||||||
|
* as opposed to some regular scheduled operation or a permanent failure.
|
||||||
|
*/
|
||||||
|
givesLifeness: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retry info, not available on all pending operations.
|
||||||
|
* If it is available, it must have the same name.
|
||||||
|
*/
|
||||||
|
retryInfo?: RetryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response returned from the pending operations API.
|
||||||
|
*/
|
||||||
|
export interface PendingOperationsResponse {
|
||||||
|
/**
|
||||||
|
* List of pending operations.
|
||||||
|
*/
|
||||||
|
pendingOperations: PendingOperationInfo[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current wallet balance, including pending balances.
|
||||||
|
*/
|
||||||
|
walletBalance: BalancesResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When is the next pending operation due to be re-tried?
|
||||||
|
*/
|
||||||
|
nextRetryDelay: Duration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does this response only include pending operations that
|
||||||
|
* are due to be executed right now?
|
||||||
|
*/
|
||||||
|
onlyDue: boolean;
|
||||||
|
}
|
337
packages/taler-wallet-core/src/types/transactionsTypes.ts
Normal file
337
packages/taler-wallet-core/src/types/transactionsTypes.ts
Normal file
@ -0,0 +1,337 @@
|
|||||||
|
/*
|
||||||
|
This file is part of GNU Taler
|
||||||
|
(C) 2019 Taler Systems S.A.
|
||||||
|
|
||||||
|
GNU Taler is free software; you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU General Public License as published by the Free Software
|
||||||
|
Foundation; either version 3, or (at your option) any later version.
|
||||||
|
|
||||||
|
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with
|
||||||
|
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type and schema definitions for the wallet's transaction list.
|
||||||
|
*
|
||||||
|
* @author Florian Dold
|
||||||
|
* @author Torsten Grote
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Imports.
|
||||||
|
*/
|
||||||
|
import { Timestamp } from "../util/time";
|
||||||
|
import {
|
||||||
|
AmountString,
|
||||||
|
Product,
|
||||||
|
InternationalizedString,
|
||||||
|
MerchantInfo,
|
||||||
|
codecForInternationalizedString,
|
||||||
|
codecForMerchantInfo,
|
||||||
|
codecForProduct,
|
||||||
|
} from "./talerTypes";
|
||||||
|
import {
|
||||||
|
Codec,
|
||||||
|
buildCodecForObject,
|
||||||
|
codecOptional,
|
||||||
|
codecForString,
|
||||||
|
codecForList,
|
||||||
|
codecForAny,
|
||||||
|
} from "../util/codec";
|
||||||
|
import { TalerErrorDetails } from "./walletTypes";
|
||||||
|
|
||||||
|
export interface TransactionsRequest {
|
||||||
|
/**
|
||||||
|
* return only transactions in the given currency
|
||||||
|
*/
|
||||||
|
currency?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if present, results will be limited to transactions related to the given search string
|
||||||
|
*/
|
||||||
|
search?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionsResponse {
|
||||||
|
// a list of past and pending transactions sorted by pending, timestamp and transactionId.
|
||||||
|
// In case two events are both pending and have the same timestamp,
|
||||||
|
// they are sorted by the transactionId
|
||||||
|
// (lexically ascending and locale-independent comparison).
|
||||||
|
transactions: Transaction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionCommon {
|
||||||
|
// opaque unique ID for the transaction, used as a starting point for paginating queries
|
||||||
|
// and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
|
||||||
|
transactionId: string;
|
||||||
|
|
||||||
|
// the type of the transaction; different types might provide additional information
|
||||||
|
type: TransactionType;
|
||||||
|
|
||||||
|
// main timestamp of the transaction
|
||||||
|
timestamp: Timestamp;
|
||||||
|
|
||||||
|
// true if the transaction is still pending, false otherwise
|
||||||
|
// If a transaction is not longer pending, its timestamp will be updated,
|
||||||
|
// but its transactionId will remain unchanged
|
||||||
|
pending: boolean;
|
||||||
|
|
||||||
|
// Raw amount of the transaction (exclusive of fees or other extra costs)
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
// Amount added or removed from the wallet's balance (including all fees and other costs)
|
||||||
|
amountEffective: AmountString;
|
||||||
|
|
||||||
|
error?: TalerErrorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Transaction =
|
||||||
|
| TransactionWithdrawal
|
||||||
|
| TransactionPayment
|
||||||
|
| TransactionRefund
|
||||||
|
| TransactionTip
|
||||||
|
| TransactionRefresh;
|
||||||
|
|
||||||
|
export enum TransactionType {
|
||||||
|
Withdrawal = "withdrawal",
|
||||||
|
Payment = "payment",
|
||||||
|
Refund = "refund",
|
||||||
|
Refresh = "refresh",
|
||||||
|
Tip = "tip",
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum WithdrawalType {
|
||||||
|
TalerBankIntegrationApi = "taler-bank-integration-api",
|
||||||
|
ManualTransfer = "manual-transfer",
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WithdrawalDetails =
|
||||||
|
| WithdrawalDetailsForManualTransfer
|
||||||
|
| WithdrawalDetailsForTalerBankIntegrationApi;
|
||||||
|
|
||||||
|
interface WithdrawalDetailsForManualTransfer {
|
||||||
|
type: WithdrawalType.ManualTransfer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payto URIs that the exchange supports.
|
||||||
|
*
|
||||||
|
* Already contains the amount and message.
|
||||||
|
*/
|
||||||
|
exchangePaytoUris: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WithdrawalDetailsForTalerBankIntegrationApi {
|
||||||
|
type: WithdrawalType.TalerBankIntegrationApi;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to true if the bank has confirmed the withdrawal, false if not.
|
||||||
|
* An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
|
||||||
|
* See also bankConfirmationUrl below.
|
||||||
|
*/
|
||||||
|
confirmed: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the withdrawal is unconfirmed, this can include a URL for user
|
||||||
|
* initiated confirmation.
|
||||||
|
*/
|
||||||
|
bankConfirmationUrl?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This should only be used for actual withdrawals
|
||||||
|
// and not for tips that have their own transactions type.
|
||||||
|
interface TransactionWithdrawal extends TransactionCommon {
|
||||||
|
type: TransactionType.Withdrawal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exchange of the withdrawal.
|
||||||
|
*/
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that got subtracted from the reserve balance.
|
||||||
|
*/
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that actually was (or will be) added to the wallet's balance.
|
||||||
|
*/
|
||||||
|
amountEffective: AmountString;
|
||||||
|
|
||||||
|
withdrawalDetails: WithdrawalDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum PaymentStatus {
|
||||||
|
/**
|
||||||
|
* Explicitly aborted after timeout / failure
|
||||||
|
*/
|
||||||
|
Aborted = "aborted",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payment failed, wallet will auto-retry.
|
||||||
|
* User should be given the option to retry now / abort.
|
||||||
|
*/
|
||||||
|
Failed = "failed",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Paid successfully
|
||||||
|
*/
|
||||||
|
Paid = "paid",
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User accepted, payment is processing.
|
||||||
|
*/
|
||||||
|
Accepted = "accepted",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TransactionPayment extends TransactionCommon {
|
||||||
|
type: TransactionType.Payment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional information about the payment.
|
||||||
|
*/
|
||||||
|
info: OrderShortInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wallet-internal end-to-end identifier for the payment.
|
||||||
|
*/
|
||||||
|
proposalId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far did the wallet get with processing the payment?
|
||||||
|
*/
|
||||||
|
status: PaymentStatus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that must be paid for the contract
|
||||||
|
*/
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount that was paid, including deposit, wire and refresh fees.
|
||||||
|
*/
|
||||||
|
amountEffective: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderShortInfo {
|
||||||
|
/**
|
||||||
|
* Order ID, uniquely identifies the order within a merchant instance
|
||||||
|
*/
|
||||||
|
orderId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hash of the contract terms.
|
||||||
|
*/
|
||||||
|
contractTermsHash: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* More information about the merchant
|
||||||
|
*/
|
||||||
|
merchant: MerchantInfo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summary of the order, given by the merchant
|
||||||
|
*/
|
||||||
|
summary: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from IETF BCP 47 language tags to localized summaries
|
||||||
|
*/
|
||||||
|
summary_i18n?: InternationalizedString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of products that are part of the order
|
||||||
|
*/
|
||||||
|
products: Product[] | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL of the fulfillment, given by the merchant
|
||||||
|
*/
|
||||||
|
fulfillmentUrl?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Plain text message that should be shown to the user
|
||||||
|
* when the payment is complete.
|
||||||
|
*/
|
||||||
|
fulfillmentMessage?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Translations of fulfillmentMessage.
|
||||||
|
*/
|
||||||
|
fulfillmentMessage_i18n?: InternationalizedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionRefund extends TransactionCommon {
|
||||||
|
type: TransactionType.Refund;
|
||||||
|
|
||||||
|
// ID for the transaction that is refunded
|
||||||
|
refundedTransactionId: string;
|
||||||
|
|
||||||
|
// Additional information about the refunded payment
|
||||||
|
info: OrderShortInfo;
|
||||||
|
|
||||||
|
// Amount that has been refunded by the merchant
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
// Amount will be added to the wallet's balance after fees and refreshing
|
||||||
|
amountEffective: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TransactionTip extends TransactionCommon {
|
||||||
|
type: TransactionType.Tip;
|
||||||
|
|
||||||
|
// Raw amount of the tip, without extra fees that apply
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
// Amount will be (or was) added to the wallet's balance after fees and refreshing
|
||||||
|
amountEffective: AmountString;
|
||||||
|
|
||||||
|
merchantBaseUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A transaction shown for refreshes that are not associated to other transactions
|
||||||
|
// such as a refresh necessary before coin expiration.
|
||||||
|
// It should only be returned by the API if the effective amount is different from zero.
|
||||||
|
interface TransactionRefresh extends TransactionCommon {
|
||||||
|
type: TransactionType.Refresh;
|
||||||
|
|
||||||
|
// Exchange that the coins are refreshed with
|
||||||
|
exchangeBaseUrl: string;
|
||||||
|
|
||||||
|
// Raw amount that is refreshed
|
||||||
|
amountRaw: AmountString;
|
||||||
|
|
||||||
|
// Amount that will be paid as fees for the refresh
|
||||||
|
amountEffective: AmountString;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const codecForTransactionsRequest = (): Codec<TransactionsRequest> =>
|
||||||
|
buildCodecForObject<TransactionsRequest>()
|
||||||
|
.property("currency", codecOptional(codecForString()))
|
||||||
|
.property("search", codecOptional(codecForString()))
|
||||||
|
.build("TransactionsRequest");
|
||||||
|
|
||||||
|
// FIXME: do full validation here!
|
||||||
|
export const codecForTransactionsResponse = (): Codec<TransactionsResponse> =>
|
||||||
|
buildCodecForObject<TransactionsResponse>()
|
||||||
|
.property("transactions", codecForList(codecForAny()))
|
||||||
|
.build("TransactionsResponse");
|
||||||
|
|
||||||
|
export const codecForOrderShortInfo = (): Codec<OrderShortInfo> =>
|
||||||
|
buildCodecForObject<OrderShortInfo>()
|
||||||
|
.property("contractTermsHash", codecForString())
|
||||||
|
.property("fulfillmentMessage", codecOptional(codecForString()))
|
||||||
|
.property(
|
||||||
|
"fulfillmentMessage_i18n",
|
||||||
|
codecOptional(codecForInternationalizedString()),
|
||||||
|
)
|
||||||
|
.property("fulfillmentUrl", codecOptional(codecForString()))
|
||||||
|
.property("merchant", codecForMerchantInfo())
|
||||||
|
.property("orderId", codecForString())
|
||||||
|
.property("products", codecOptional(codecForList(codecForProduct())))
|
||||||
|
.property("summary", codecForString())
|
||||||
|
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
|
||||||
|
.build("OrderShortInfo");
|
Loading…
Reference in New Issue
Block a user